mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
improvement(marketplace): loading and viewing
This commit is contained in:
@@ -1,126 +0,0 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { getSession } from '@/lib/auth'
|
||||
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('MarketplaceStarAPI')
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
const userId = session?.user?.id
|
||||
|
||||
if (!userId) {
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Check if the marketplace entry exists
|
||||
const marketplaceEntry = await db
|
||||
.select()
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Check if the user has already starred this workflow
|
||||
const existingStar = await db
|
||||
.select()
|
||||
.from(schema.marketplaceStar)
|
||||
.where(
|
||||
and(eq(schema.marketplaceStar.marketplaceId, id), eq(schema.marketplaceStar.userId, userId))
|
||||
)
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
let action
|
||||
if (existingStar) {
|
||||
// User has already starred, so unstar it
|
||||
await db
|
||||
.delete(schema.marketplaceStar)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.marketplaceStar.marketplaceId, id),
|
||||
eq(schema.marketplaceStar.userId, userId)
|
||||
)
|
||||
)
|
||||
|
||||
// Decrement the star count
|
||||
await db
|
||||
.update(schema.marketplace)
|
||||
.set({ stars: marketplaceEntry.stars - 1 })
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
|
||||
action = 'unstarred'
|
||||
} else {
|
||||
// User hasn't starred yet, add a star
|
||||
await db.insert(schema.marketplaceStar).values({
|
||||
id: crypto.randomUUID(),
|
||||
marketplaceId: id,
|
||||
userId: userId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
// Increment the star count
|
||||
await db
|
||||
.update(schema.marketplace)
|
||||
.set({ stars: marketplaceEntry.stars + 1 })
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
|
||||
action = 'starred'
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] User ${userId} ${action} marketplace entry: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
action,
|
||||
stars: action === 'starred' ? marketplaceEntry.stars + 1 : marketplaceEntry.stars - 1,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error starring marketplace entry: ${(await params).id}`, error)
|
||||
return createErrorResponse('Failed to update star status', 500)
|
||||
}
|
||||
}
|
||||
|
||||
// GET endpoint to check if user has starred a workflow
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const session = await getSession()
|
||||
const userId = session?.user?.id
|
||||
|
||||
if (!userId) {
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Check if the user has already starred this workflow
|
||||
const existingStar = await db
|
||||
.select()
|
||||
.from(schema.marketplaceStar)
|
||||
.where(
|
||||
and(eq(schema.marketplaceStar.marketplaceId, id), eq(schema.marketplaceStar.userId, userId))
|
||||
)
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
return createSuccessResponse({
|
||||
isStarred: !!existingStar,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error checking star status: ${(await params).id}`, error)
|
||||
return createErrorResponse('Failed to check star status', 500)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
57
sim/app/api/marketplace/[id]/view/route.ts
Normal file
57
sim/app/api/marketplace/[id]/view/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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('MarketplaceViewAPI')
|
||||
|
||||
/**
|
||||
* POST handler for incrementing the view count when a workflow card is clicked
|
||||
* This endpoint is called from the WorkflowCard component's onClick handler
|
||||
*
|
||||
* The ID parameter is the marketplace entry ID, not the workflow ID
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Find the marketplace entry for this marketplace ID
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Increment the view count for this workflow
|
||||
await db
|
||||
.update(schema.marketplace)
|
||||
.set({
|
||||
views: sql`${schema.marketplace.views} + 1`
|
||||
})
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Incremented view count for marketplace entry: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[${requestId}] Error incrementing view count for marketplace entry: ${(await params).id}`,
|
||||
error
|
||||
)
|
||||
return createErrorResponse('Failed to track view', 500)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
353
sim/app/api/marketplace/workflows/route.ts
Normal file
353
sim/app/api/marketplace/workflows/route.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { desc, 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'
|
||||
import { CATEGORIES } from '@/app/w/marketplace/constants/categories'
|
||||
|
||||
const logger = createLogger('MarketplaceWorkflowsAPI')
|
||||
|
||||
// Cache for 1 minute but can be revalidated on-demand
|
||||
export const revalidate = 60
|
||||
|
||||
/**
|
||||
* Consolidated API endpoint for marketplace workflows
|
||||
*
|
||||
* Supports:
|
||||
* - Getting featured/popular/recent workflows
|
||||
* - Getting workflows by category
|
||||
* - Getting workflow state
|
||||
* - Getting workflow details
|
||||
* - Incrementing view counts
|
||||
*
|
||||
* Query parameters:
|
||||
* - section: 'popular', 'recent', 'byCategory', or specific category name
|
||||
* - limit: Maximum number of items to return per section (default: 6)
|
||||
* - includeState: Whether to include workflow state in the response (default: false)
|
||||
* - workflowId: Specific workflow ID to fetch details for
|
||||
* - marketplaceId: Specific marketplace entry ID to fetch details for
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
// Parse query parameters
|
||||
const url = new URL(request.url)
|
||||
const sectionParam = url.searchParams.get('section')
|
||||
const categoryParam = url.searchParams.get('category')
|
||||
const limitParam = url.searchParams.get('limit') || '6'
|
||||
const limit = parseInt(limitParam, 10)
|
||||
const includeState = url.searchParams.get('includeState') === 'true'
|
||||
const workflowId = url.searchParams.get('workflowId')
|
||||
const marketplaceId = url.searchParams.get('marketplaceId')
|
||||
|
||||
// Handle single workflow request first (by workflow ID)
|
||||
if (workflowId) {
|
||||
let marketplaceEntry;
|
||||
|
||||
if (includeState) {
|
||||
// Query with state included
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
state: schema.marketplace.state,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
updatedAt: schema.marketplace.updatedAt,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.workflowId, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]);
|
||||
} else {
|
||||
// Query without state
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
updatedAt: schema.marketplace.updatedAt,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.workflowId, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]);
|
||||
}
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${workflowId}`)
|
||||
return createErrorResponse('Workflow not found in marketplace', 404)
|
||||
}
|
||||
|
||||
// Transform response if state was requested
|
||||
const responseData = includeState && 'state' in marketplaceEntry
|
||||
? {
|
||||
...marketplaceEntry,
|
||||
workflowState: marketplaceEntry.state,
|
||||
state: undefined,
|
||||
}
|
||||
: marketplaceEntry;
|
||||
|
||||
logger.info(`[${requestId}] Retrieved marketplace data for workflow: ${workflowId}`)
|
||||
return createSuccessResponse(responseData)
|
||||
}
|
||||
|
||||
// Handle single marketplace entry request (by marketplace ID)
|
||||
if (marketplaceId) {
|
||||
let marketplaceEntry;
|
||||
|
||||
if (includeState) {
|
||||
// Query with state included
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
state: schema.marketplace.state,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
updatedAt: schema.marketplace.updatedAt,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.id, marketplaceId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]);
|
||||
} else {
|
||||
// Query without state
|
||||
marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
updatedAt: schema.marketplace.updatedAt,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.id, marketplaceId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]);
|
||||
}
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${marketplaceId}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Transform response if state was requested
|
||||
const responseData = includeState && 'state' in marketplaceEntry
|
||||
? {
|
||||
...marketplaceEntry,
|
||||
workflowState: marketplaceEntry.state,
|
||||
state: undefined,
|
||||
}
|
||||
: marketplaceEntry;
|
||||
|
||||
logger.info(`[${requestId}] Retrieved marketplace entry: ${marketplaceId}`)
|
||||
return createSuccessResponse(responseData)
|
||||
}
|
||||
|
||||
// Handle featured/collection requests
|
||||
const result: {
|
||||
popular: any[]
|
||||
recent: any[]
|
||||
byCategory: Record<string, any[]>
|
||||
} = {
|
||||
popular: [],
|
||||
recent: [],
|
||||
byCategory: {}
|
||||
}
|
||||
|
||||
// Define common fields to select
|
||||
const baseFields = {
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
updatedAt: schema.marketplace.updatedAt,
|
||||
}
|
||||
|
||||
// Add state if requested
|
||||
const selectFields = includeState
|
||||
? { ...baseFields, state: schema.marketplace.state }
|
||||
: baseFields;
|
||||
|
||||
// Determine which sections to fetch
|
||||
const sections = sectionParam ? sectionParam.split(',') : ['popular', 'recent', 'byCategory']
|
||||
|
||||
// Get popular items if requested
|
||||
if (sections.includes('popular')) {
|
||||
result.popular = await db
|
||||
.select(selectFields)
|
||||
.from(schema.marketplace)
|
||||
.orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
// Get recent items if requested
|
||||
if (sections.includes('recent')) {
|
||||
result.recent = await db
|
||||
.select(selectFields)
|
||||
.from(schema.marketplace)
|
||||
.orderBy(desc(schema.marketplace.createdAt))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
// Get categories if requested
|
||||
if (sections.includes('byCategory') || categoryParam || sections.some(s => CATEGORIES.some(c => c.value === s))) {
|
||||
// Identify all requested categories
|
||||
const requestedCategories = new Set<string>();
|
||||
|
||||
// Add explicitly requested category
|
||||
if (categoryParam) {
|
||||
requestedCategories.add(categoryParam);
|
||||
}
|
||||
|
||||
// Add categories from sections parameter
|
||||
sections.forEach(section => {
|
||||
if (CATEGORIES.some(c => c.value === section)) {
|
||||
requestedCategories.add(section);
|
||||
}
|
||||
});
|
||||
|
||||
// Include byCategory section contents if requested
|
||||
if (sections.includes('byCategory')) {
|
||||
CATEGORIES.forEach(c => requestedCategories.add(c.value));
|
||||
}
|
||||
|
||||
// Log what we're fetching
|
||||
const categoriesToFetch = Array.from(requestedCategories);
|
||||
logger.info(`[${requestId}] Fetching specific categories: ${categoriesToFetch.join(', ')}`);
|
||||
|
||||
// Process each requested category
|
||||
await Promise.all(
|
||||
categoriesToFetch.map(async (categoryValue) => {
|
||||
const categoryItems = await db
|
||||
.select(selectFields)
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.category, categoryValue))
|
||||
.orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views))
|
||||
.limit(limit);
|
||||
|
||||
// Always add the category to the result, even if empty
|
||||
result.byCategory[categoryValue] = categoryItems;
|
||||
logger.info(`[${requestId}] Category ${categoryValue}: found ${categoryItems.length} items`);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Transform the data if state was included to match the expected format
|
||||
if (includeState) {
|
||||
const transformSection = (section: any[]) => {
|
||||
return section.map(item =>
|
||||
'state' in item ? {
|
||||
...item,
|
||||
workflowState: item.state,
|
||||
state: undefined
|
||||
} : item
|
||||
);
|
||||
};
|
||||
|
||||
if (result.popular.length > 0) {
|
||||
result.popular = transformSection(result.popular);
|
||||
}
|
||||
|
||||
if (result.recent.length > 0) {
|
||||
result.recent = transformSection(result.recent);
|
||||
}
|
||||
|
||||
Object.keys(result.byCategory).forEach(category => {
|
||||
if (result.byCategory[category].length > 0) {
|
||||
result.byCategory[category] = transformSection(result.byCategory[category]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Fetched marketplace items${includeState ? ' with state' : ''}`)
|
||||
return NextResponse.json(result)
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error fetching marketplace items`, error)
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for incrementing view counts
|
||||
*
|
||||
* Request body:
|
||||
* - id: Marketplace entry ID to increment view count for
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { id } = body
|
||||
|
||||
if (!id) {
|
||||
return createErrorResponse('Marketplace ID is required', 400)
|
||||
}
|
||||
|
||||
// Find the marketplace entry
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Increment the view count
|
||||
await db
|
||||
.update(schema.marketplace)
|
||||
.set({
|
||||
views: sql`${schema.marketplace.views} + 1`
|
||||
})
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Incremented view count for marketplace entry: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error incrementing view count`, error)
|
||||
return createErrorResponse(`Failed to track view: ${error.message}`, 500)
|
||||
}
|
||||
}
|
||||
68
sim/app/api/workflows/public/[id]/route.ts
Normal file
68
sim/app/api/workflows/public/[id]/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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('PublicWorkflowAPI')
|
||||
|
||||
// Cache response for performance
|
||||
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
|
||||
|
||||
// First, check if the workflow exists and is published to the marketplace
|
||||
const marketplaceEntry = await db
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
state: schema.marketplace.state,
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.workflowId, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
// Check if workflow exists but is not in marketplace
|
||||
const workflowExists = await db
|
||||
.select({ id: schema.workflow.id })
|
||||
.from(schema.workflow)
|
||||
.where(eq(schema.workflow.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows.length > 0)
|
||||
|
||||
if (!workflowExists) {
|
||||
logger.warn(`[${requestId}] Workflow not found: ${id}`)
|
||||
return createErrorResponse('Workflow not found', 404)
|
||||
}
|
||||
|
||||
logger.warn(`[${requestId}] Workflow exists but is not published: ${id}`)
|
||||
return createErrorResponse('Workflow is not published', 403)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Retrieved public workflow: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
id: marketplaceEntry.workflowId,
|
||||
name: marketplaceEntry.name,
|
||||
description: marketplaceEntry.description,
|
||||
authorId: marketplaceEntry.authorId,
|
||||
authorName: marketplaceEntry.authorName,
|
||||
state: marketplaceEntry.state,
|
||||
isPublic: true,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error getting public workflow: ${(await params).id}`, error)
|
||||
return createErrorResponse('Failed to get public workflow', 500)
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,9 @@ function WorkflowContent() {
|
||||
// State
|
||||
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [isPublicWorkflow, setIsPublicWorkflow] = useState(false)
|
||||
const [publicWorkflowData, setPublicWorkflowData] = useState<any>(null)
|
||||
const [loadingPublicWorkflow, setLoadingPublicWorkflow] = useState(false)
|
||||
|
||||
// Hooks
|
||||
const params = useParams()
|
||||
@@ -51,8 +54,16 @@ function WorkflowContent() {
|
||||
|
||||
// Store access
|
||||
const { workflows, setActiveWorkflow, createWorkflow } = useWorkflowRegistry()
|
||||
const { blocks, edges, loops, addBlock, updateBlockPosition, addEdge, removeEdge } =
|
||||
useWorkflowStore()
|
||||
const {
|
||||
blocks,
|
||||
edges,
|
||||
loops,
|
||||
addBlock,
|
||||
updateBlockPosition,
|
||||
addEdge,
|
||||
removeEdge,
|
||||
initializeWorkflow,
|
||||
} = useWorkflowStore()
|
||||
const { setValue: setSubBlockValue } = useSubBlockStore()
|
||||
const { markAllAsRead } = useNotificationStore()
|
||||
const { resetLoaded: resetVariablesLoaded } = useVariablesStore()
|
||||
@@ -151,9 +162,47 @@ function WorkflowContent() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the workflow is in the user's registry
|
||||
if (!workflows[currentId]) {
|
||||
router.replace(`/w/${workflowIds[0]}`)
|
||||
return
|
||||
// If not in registry, try to load it as a public workflow
|
||||
setLoadingPublicWorkflow(true)
|
||||
try {
|
||||
const response = await fetch(`/api/workflows/public/${currentId}`)
|
||||
|
||||
if (response.ok) {
|
||||
// Workflow exists and is public
|
||||
const data = await response.json()
|
||||
setPublicWorkflowData(data.data)
|
||||
setIsPublicWorkflow(true)
|
||||
|
||||
// Initialize the workflow store with the public workflow state
|
||||
if (data.data?.state) {
|
||||
const { blocks, edges, loops } = data.data.state
|
||||
initializeWorkflow(blocks || {}, edges || [], loops || {})
|
||||
|
||||
// Initialize subblock store with workflow values
|
||||
if (blocks) {
|
||||
useSubBlockStore.getState().initializeFromWorkflow(currentId, blocks)
|
||||
}
|
||||
}
|
||||
setLoadingPublicWorkflow(false)
|
||||
return
|
||||
} else if (response.status === 403) {
|
||||
// Workflow exists but is not public, redirect to first workflow
|
||||
router.replace(`/w/${workflowIds[0]}`)
|
||||
return
|
||||
} else {
|
||||
// Workflow doesn't exist, redirect to first workflow
|
||||
router.replace(`/w/${workflowIds[0]}`)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading public workflow:', error)
|
||||
router.replace(`/w/${workflowIds[0]}`)
|
||||
return
|
||||
} finally {
|
||||
setLoadingPublicWorkflow(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Import the isActivelyLoadingFromDB function to check sync status
|
||||
@@ -190,6 +239,7 @@ function WorkflowContent() {
|
||||
isInitialized,
|
||||
markAllAsRead,
|
||||
resetVariablesLoaded,
|
||||
initializeWorkflow,
|
||||
])
|
||||
|
||||
// Transform blocks and loops into ReactFlow nodes
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Eye, Star } from 'lucide-react'
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||
import { Workflow } from '../marketplace'
|
||||
@@ -21,78 +22,110 @@ interface WorkflowCardProps {
|
||||
/**
|
||||
* WorkflowCard component - Displays a workflow in a card format
|
||||
* Shows either a workflow preview, thumbnail image, or fallback text
|
||||
* Loads workflow state on hover if not already loaded
|
||||
* State is now pre-loaded in most cases, fallback to load on hover if needed
|
||||
*/
|
||||
export function WorkflowCard({ workflow, index, onHover }: WorkflowCardProps) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
const [isPreviewReady, setIsPreviewReady] = useState(!!workflow.workflowState)
|
||||
|
||||
// When workflow state becomes available, update preview ready state
|
||||
useEffect(() => {
|
||||
if (workflow.workflowState && !isPreviewReady) {
|
||||
setIsPreviewReady(true)
|
||||
}
|
||||
}, [workflow.workflowState, isPreviewReady])
|
||||
|
||||
/**
|
||||
* Handle mouse enter event
|
||||
* Sets hover state and triggers onHover callback to load workflow state if needed
|
||||
*/
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true)
|
||||
if (onHover && !workflow.workflowState) {
|
||||
onHover(workflow.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle workflow card click - track views
|
||||
*/
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await fetch(`/api/marketplace/workflows`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: workflow.id }),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to track workflow view:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the actual workflow ID from workflow
|
||||
const workflowUrl = workflow.workflowUrl || `/w/${workflow.id}`
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="overflow-hidden transition-all hover:shadow-md flex flex-col h-full"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
<Link
|
||||
href={workflowUrl}
|
||||
className="block"
|
||||
aria-label={`View ${workflow.name} workflow`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* Workflow preview/thumbnail area */}
|
||||
<div className="h-40 relative overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900">
|
||||
{workflow.workflowState ? (
|
||||
// Show interactive workflow preview if state is available
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-full h-full transform-gpu scale-[0.8]">
|
||||
<WorkflowPreview workflowState={workflow.workflowState} />
|
||||
<Card
|
||||
className="overflow-hidden transition-all hover:shadow-md flex flex-col h-full"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{/* Workflow preview/thumbnail area */}
|
||||
<div className="h-40 relative overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900">
|
||||
{isPreviewReady && workflow.workflowState ? (
|
||||
// Show interactive workflow preview if state is available
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-full h-full transform-gpu scale-[0.8]">
|
||||
<WorkflowPreview workflowState={workflow.workflowState} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : workflow.thumbnail ? (
|
||||
// Show static thumbnail image if available
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${workflow.thumbnail})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center top',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// Fallback to text if no preview or thumbnail is available
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<span className="text-muted-foreground font-medium text-lg">{workflow.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
{/* Workflow title */}
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<h3 className="font-medium text-sm">{workflow.name}</h3>
|
||||
</CardHeader>
|
||||
{/* Workflow description */}
|
||||
<CardContent className="p-4 pt-0 pb-2 flex-grow flex flex-col">
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{workflow.description}</p>
|
||||
</CardContent>
|
||||
{/* Footer with author and stats */}
|
||||
<CardFooter className="p-4 pt-2 mt-auto flex justify-between items-center">
|
||||
<div className="text-xs text-muted-foreground">by {workflow.author}</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.stars}</span>
|
||||
) : workflow.thumbnail ? (
|
||||
// Show static thumbnail image if available
|
||||
<div
|
||||
className="h-full w-full bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${workflow.thumbnail})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center top',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// Fallback to text if no preview or thumbnail is available
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<span className="text-muted-foreground font-medium text-lg">{workflow.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.views}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
{/* Workflow title */}
|
||||
<CardHeader className="p-4 pb-2">
|
||||
<h3 className="font-medium text-sm">{workflow.name}</h3>
|
||||
</CardHeader>
|
||||
{/* Workflow description */}
|
||||
<CardContent className="p-4 pt-0 pb-2 flex-grow flex flex-col">
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{workflow.description}</p>
|
||||
</CardContent>
|
||||
{/* Footer with author and stats */}
|
||||
<CardFooter className="p-4 pt-2 mt-auto flex justify-between items-center">
|
||||
<div className="text-xs text-muted-foreground">by {workflow.author}</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.stars}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.views}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Section } from './components/section'
|
||||
import { Toolbar } from './components/toolbar/toolbar'
|
||||
import { WorkflowCard } from './components/workflow-card'
|
||||
import { WorkflowCardSkeleton } from './components/workflow-card-skeleton'
|
||||
import { CATEGORIES } from './constants/categories'
|
||||
|
||||
// Types
|
||||
export interface Workflow {
|
||||
@@ -22,7 +23,7 @@ export interface Workflow {
|
||||
views: number
|
||||
tags: string[]
|
||||
thumbnail?: string
|
||||
workflowUrl?: string
|
||||
workflowUrl: string
|
||||
workflowState?: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<{
|
||||
@@ -67,6 +68,9 @@ export interface MarketplaceData {
|
||||
byCategory: Record<string, MarketplaceWorkflow[]>
|
||||
}
|
||||
|
||||
// The order to display sections in, matching toolbar order
|
||||
const SECTION_ORDER = ['popular', 'recent', ...CATEGORIES.map((cat) => cat.value)]
|
||||
|
||||
export default function Marketplace() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -77,10 +81,13 @@ export default function Marketplace() {
|
||||
byCategory: {},
|
||||
})
|
||||
const [activeSection, setActiveSection] = useState<string | null>(null)
|
||||
const [loadedSections, setLoadedSections] = useState<Set<string>>(new Set(['popular', 'recent']))
|
||||
const [visibleSections, setVisibleSections] = useState<Set<string>>(new Set(['popular']))
|
||||
|
||||
// Create refs for each section
|
||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const initialFetchCompleted = useRef(false)
|
||||
|
||||
// Convert marketplace data to the format expected by components
|
||||
const workflowData = useMemo(() => {
|
||||
@@ -94,6 +101,7 @@ export default function Marketplace() {
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
workflowUrl: `/w/${item.workflowId}`,
|
||||
})),
|
||||
recent: marketplaceData.recent.map((item) => ({
|
||||
id: item.id,
|
||||
@@ -104,6 +112,7 @@ export default function Marketplace() {
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
workflowUrl: `/w/${item.workflowId}`,
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -119,6 +128,7 @@ export default function Marketplace() {
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
workflowUrl: `/w/${item.workflowId}`,
|
||||
}))
|
||||
}
|
||||
})
|
||||
@@ -126,22 +136,46 @@ export default function Marketplace() {
|
||||
return result
|
||||
}, [marketplaceData])
|
||||
|
||||
// Fetch workflows on component mount
|
||||
// Fetch workflows on component mount - improved to include state initially
|
||||
useEffect(() => {
|
||||
const fetchWorkflows = async () => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const response = await fetch('/api/marketplace/featured')
|
||||
// Fetch ALL data including categories in the initial load
|
||||
const response = await fetch(
|
||||
'/api/marketplace/workflows?includeState=true§ion=popular,recent,byCategory'
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch marketplace data')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setMarketplaceData(data)
|
||||
|
||||
// Set initial active section to first category
|
||||
// Add all categories to loaded sections to avoid redundant load
|
||||
setLoadedSections((prev) => {
|
||||
const allSections = new Set([...prev])
|
||||
Object.keys(data.byCategory || {}).forEach((category) => {
|
||||
allSections.add(category)
|
||||
})
|
||||
return allSections
|
||||
})
|
||||
|
||||
console.log(
|
||||
'Initial marketplace data loaded with categories:',
|
||||
data.popular?.length || 0,
|
||||
'popular,',
|
||||
data.recent?.length || 0,
|
||||
'recent,',
|
||||
'categories:',
|
||||
Object.keys(data.byCategory || {})
|
||||
)
|
||||
|
||||
setMarketplaceData(data)
|
||||
initialFetchCompleted.current = true
|
||||
|
||||
// Set initial active section to popular
|
||||
setActiveSection('popular')
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
@@ -151,11 +185,61 @@ export default function Marketplace() {
|
||||
}
|
||||
}
|
||||
|
||||
fetchWorkflows()
|
||||
fetchInitialData()
|
||||
}, [])
|
||||
|
||||
// Function to fetch workflow state for a specific workflow
|
||||
const fetchWorkflowState = async (workflowId: string) => {
|
||||
// Lazy load category data when sections become visible
|
||||
const loadCategoryData = async (categoryName: string) => {
|
||||
if (loadedSections.has(categoryName)) {
|
||||
return // Already loaded, no need to fetch again
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadedSections((prev) => new Set([...prev, categoryName]))
|
||||
|
||||
console.log(`Loading category: ${categoryName}`) // Debug
|
||||
|
||||
const response = await fetch(
|
||||
`/api/marketplace/workflows?includeState=true&category=${categoryName}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${categoryName} category data`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Debug logging
|
||||
console.log(
|
||||
`Category data received:`,
|
||||
data.byCategory ? Object.keys(data.byCategory) : 'No byCategory',
|
||||
data.byCategory?.[categoryName]?.length || 0
|
||||
)
|
||||
|
||||
// Check if we received any data in the category
|
||||
if (
|
||||
!data.byCategory ||
|
||||
!data.byCategory[categoryName] ||
|
||||
data.byCategory[categoryName].length === 0
|
||||
) {
|
||||
console.warn(`No items found for category: ${categoryName}`)
|
||||
}
|
||||
|
||||
setMarketplaceData((prev) => ({
|
||||
...prev,
|
||||
byCategory: {
|
||||
...prev.byCategory,
|
||||
[categoryName]: data.byCategory?.[categoryName] || [],
|
||||
},
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${categoryName} category:`, error)
|
||||
// We don't set a global error, just log it
|
||||
}
|
||||
}
|
||||
|
||||
// Function to mark a workflow as needing state and fetch it if not available
|
||||
const ensureWorkflowState = async (workflowId: string) => {
|
||||
try {
|
||||
// Find which section contains this workflow
|
||||
let foundWorkflow: MarketplaceWorkflow | undefined
|
||||
@@ -176,35 +260,44 @@ export default function Marketplace() {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have the workflow and it doesn't already have state
|
||||
// If we have the workflow but it doesn't have state, fetch it
|
||||
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',
|
||||
})
|
||||
const response = await fetch(
|
||||
`/api/marketplace/workflows?marketplaceId=${workflowId}&includeState=true`,
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const stateData = await response.json()
|
||||
const data = await response.json()
|
||||
|
||||
// Update the workflow data with the state
|
||||
setMarketplaceData((prevData) => {
|
||||
const updatedData = { ...prevData }
|
||||
|
||||
// Helper function to update workflow in a section
|
||||
const updateWorkflowInSection = (workflows: MarketplaceWorkflow[]) => {
|
||||
return workflows.map((w) =>
|
||||
w.id === workflowId
|
||||
? {
|
||||
...w,
|
||||
workflowState: data.data.workflowState,
|
||||
}
|
||||
: w
|
||||
)
|
||||
}
|
||||
|
||||
// Update in popular
|
||||
updatedData.popular = updatedData.popular.map((w) =>
|
||||
w.id === workflowId ? { ...w, workflowState: stateData.state } : w
|
||||
)
|
||||
updatedData.popular = updateWorkflowInSection(updatedData.popular)
|
||||
|
||||
// Update in recent
|
||||
updatedData.recent = updatedData.recent.map((w) =>
|
||||
w.id === workflowId ? { ...w, workflowState: stateData.state } : w
|
||||
)
|
||||
updatedData.recent = updateWorkflowInSection(updatedData.recent)
|
||||
|
||||
// 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
|
||||
updatedData.byCategory[category] = updateWorkflowInSection(
|
||||
updatedData.byCategory[category]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -213,7 +306,7 @@ export default function Marketplace() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching workflow state for ${workflowId}:`, error)
|
||||
console.error(`Error ensuring workflow state for ${workflowId}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +336,40 @@ export default function Marketplace() {
|
||||
return filtered
|
||||
}, [searchQuery, workflowData])
|
||||
|
||||
// Sort sections according to the toolbar order
|
||||
const sortedFilteredWorkflows = useMemo(() => {
|
||||
// Get entries from filteredWorkflows
|
||||
const entries = Object.entries(filteredWorkflows)
|
||||
|
||||
// Sort based on the SECTION_ORDER
|
||||
entries.sort((a, b) => {
|
||||
const indexA = SECTION_ORDER.indexOf(a[0])
|
||||
const indexB = SECTION_ORDER.indexOf(b[0])
|
||||
|
||||
// If both categories are in our predefined order, use that order
|
||||
if (indexA !== -1 && indexB !== -1) {
|
||||
return indexA - indexB
|
||||
}
|
||||
|
||||
// If only one category is in our order, prioritize it
|
||||
if (indexA !== -1) return -1
|
||||
if (indexB !== -1) return 1
|
||||
|
||||
// Otherwise, alphabetical order
|
||||
return a[0].localeCompare(b[0])
|
||||
})
|
||||
|
||||
return entries
|
||||
}, [filteredWorkflows])
|
||||
|
||||
// Function to scroll to a specific section
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
if (sectionRefs.current[sectionId]) {
|
||||
// Load the section data if not already loaded
|
||||
if (!loadedSections.has(sectionId) && sectionId !== 'popular' && sectionId !== 'recent') {
|
||||
loadCategoryData(sectionId)
|
||||
}
|
||||
|
||||
sectionRefs.current[sectionId]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
@@ -253,8 +377,10 @@ export default function Marketplace() {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup intersection observer to track active section
|
||||
// Setup intersection observer to track active section and load sections as they become visible
|
||||
useEffect(() => {
|
||||
if (!initialFetchCompleted.current) return
|
||||
|
||||
// Function to get current section IDs in their display order
|
||||
const getCurrentSectionIds = () => {
|
||||
return Object.keys(filteredWorkflows).filter(
|
||||
@@ -262,31 +388,56 @@ export default function Marketplace() {
|
||||
)
|
||||
}
|
||||
|
||||
// Store current section positions for better tracking
|
||||
const sectionPositions: Record<string, DOMRect> = {}
|
||||
// Create intersection observer to detect when sections enter viewport
|
||||
const observeSections = () => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const sectionId = entry.target.id
|
||||
|
||||
// Initial calculation of section positions
|
||||
const calculateSectionPositions = () => {
|
||||
// Update visibility tracking
|
||||
if (entry.isIntersecting) {
|
||||
setVisibleSections((prev) => {
|
||||
const updated = new Set(prev)
|
||||
updated.add(sectionId)
|
||||
return updated
|
||||
})
|
||||
|
||||
// Load category data if section is visible and not loaded yet
|
||||
if (
|
||||
!loadedSections.has(sectionId) &&
|
||||
sectionId !== 'popular' &&
|
||||
sectionId !== 'recent'
|
||||
) {
|
||||
loadCategoryData(sectionId)
|
||||
}
|
||||
} else {
|
||||
setVisibleSections((prev) => {
|
||||
const updated = new Set(prev)
|
||||
updated.delete(sectionId)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
root: contentRef.current,
|
||||
rootMargin: '200px 0px', // Load sections slightly before they become visible
|
||||
threshold: 0.1,
|
||||
}
|
||||
)
|
||||
|
||||
// Observe all sections
|
||||
Object.entries(sectionRefs.current).forEach(([id, ref]) => {
|
||||
if (ref) {
|
||||
sectionPositions[id] = ref.getBoundingClientRect()
|
||||
observer.observe(ref)
|
||||
}
|
||||
})
|
||||
|
||||
return observer
|
||||
}
|
||||
|
||||
// Debounce function to limit rapid position calculations
|
||||
const debounce = (fn: Function, delay: number) => {
|
||||
let timer: NodeJS.Timeout
|
||||
return function (...args: any[]) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => fn(...args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate positions initially and on resize
|
||||
calculateSectionPositions()
|
||||
const debouncedCalculatePositions = debounce(calculateSectionPositions, 100)
|
||||
window.addEventListener('resize', debouncedCalculatePositions)
|
||||
const observer = observeSections()
|
||||
|
||||
// Use a single source of truth for determining the active section
|
||||
const determineActiveSection = () => {
|
||||
@@ -347,10 +498,10 @@ export default function Marketplace() {
|
||||
contentElement?.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', debouncedCalculatePositions)
|
||||
observer.disconnect()
|
||||
contentElement?.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [loading, filteredWorkflows])
|
||||
}, [initialFetchCompleted.current, loading, filteredWorkflows, loadedSections])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[100vh]">
|
||||
@@ -388,7 +539,7 @@ export default function Marketplace() {
|
||||
{/* Render workflow sections */}
|
||||
{!loading && (
|
||||
<>
|
||||
{Object.entries(filteredWorkflows).map(
|
||||
{sortedFilteredWorkflows.map(
|
||||
([category, workflows]) =>
|
||||
workflows.length > 0 && (
|
||||
<Section
|
||||
@@ -407,7 +558,7 @@ export default function Marketplace() {
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
index={index}
|
||||
onHover={fetchWorkflowState}
|
||||
onHover={ensureWorkflowState}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -415,7 +566,7 @@ export default function Marketplace() {
|
||||
)
|
||||
)}
|
||||
|
||||
{Object.keys(filteredWorkflows).length === 0 && !loading && (
|
||||
{sortedFilteredWorkflows.length === 0 && !loading && (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<AlertCircle className="h-8 w-8 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground">No workflows found matching your search.</p>
|
||||
|
||||
@@ -124,7 +124,7 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
|
||||
description: 'Evaluate content',
|
||||
longDescription:
|
||||
'Assess content quality using customizable evaluation metrics and scoring criteria. Create objective evaluation frameworks with numeric scoring to measure performance across multiple dimensions.',
|
||||
category: 'blocks',
|
||||
category: 'tools',
|
||||
bgColor: '#2FA1FF',
|
||||
icon: ChartBarIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -306,6 +306,23 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize workflow from external state (used for public workflows)
|
||||
initializeWorkflow: (blocks: any, edges: any, loops: any) => {
|
||||
const newState = {
|
||||
blocks: blocks || {},
|
||||
edges: edges || [],
|
||||
loops: loops || {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
isPublished: true,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
// Don't push to history since this is an initial load
|
||||
// Don't sync to avoid overwriting the original workflow
|
||||
},
|
||||
|
||||
toggleBlockEnabled: (id: string) => {
|
||||
const newState = {
|
||||
blocks: {
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface WorkflowActions {
|
||||
triggerUpdate: () => void
|
||||
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
|
||||
setPublishStatus: (isPublished: boolean) => void
|
||||
initializeWorkflow: (blocks: any, edges: any, loops: any) => void
|
||||
}
|
||||
|
||||
export type WorkflowStore = WorkflowState & WorkflowActions
|
||||
|
||||
Reference in New Issue
Block a user