kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add project.tags and filtering based on tags for public projects
rodzic
de80f97d93
commit
07e5887dc9
|
@ -4,14 +4,24 @@ import type { DefaultHonoEnv } from '@agentic/platform-hono'
|
|||
import { parseZodSchema } from '@agentic/platform-core'
|
||||
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||
|
||||
import { and, db, eq, isNotNull, schema } from '@/db'
|
||||
import {
|
||||
and,
|
||||
arrayContains,
|
||||
db,
|
||||
eq,
|
||||
isNotNull,
|
||||
isNull,
|
||||
not,
|
||||
or,
|
||||
schema
|
||||
} from '@/db'
|
||||
import { setPublicCacheControl } from '@/lib/cache-control'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponses
|
||||
} from '@/lib/openapi-utils'
|
||||
|
||||
import { paginationAndPopulateProjectSchema } from './schemas'
|
||||
import { listPublicProjectsQuerySchema } from './schemas'
|
||||
|
||||
const route = createRoute({
|
||||
description:
|
||||
|
@ -22,7 +32,7 @@ const route = createRoute({
|
|||
path: 'projects/public',
|
||||
security: openapiAuthenticatedSecuritySchemas,
|
||||
request: {
|
||||
query: paginationAndPopulateProjectSchema
|
||||
query: listPublicProjectsQuerySchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
|
@ -44,14 +54,24 @@ export function registerV1ListPublicProjects(app: OpenAPIHono<DefaultHonoEnv>) {
|
|||
limit = 10,
|
||||
sort = 'desc',
|
||||
sortBy = 'createdAt',
|
||||
populate = []
|
||||
populate = [],
|
||||
tag,
|
||||
notTag
|
||||
} = c.req.valid('query')
|
||||
|
||||
const projects = await db.query.projects.findMany({
|
||||
// List projects that are not private and have at least one published deployment
|
||||
// And optionally match a given tag
|
||||
where: and(
|
||||
eq(schema.projects.private, false),
|
||||
isNotNull(schema.projects.lastPublishedDeploymentId)
|
||||
isNotNull(schema.projects.lastPublishedDeploymentId),
|
||||
tag ? arrayContains(schema.projects.tags, [tag]) : undefined,
|
||||
notTag
|
||||
? or(
|
||||
not(arrayContains(schema.projects.tags, [notTag])),
|
||||
isNull(schema.projects.tags)
|
||||
)
|
||||
: undefined
|
||||
),
|
||||
with: {
|
||||
lastPublishedDeployment: true,
|
||||
|
|
|
@ -37,6 +37,11 @@ export const projectIdentifierQuerySchema = z.object({
|
|||
projectIdentifier: projectIdentifierSchema
|
||||
})
|
||||
|
||||
export const filterPublicProjectSchema = z.object({
|
||||
tag: z.string().optional(),
|
||||
notTag: z.string().optional()
|
||||
})
|
||||
|
||||
export const populateProjectSchema = z.object({
|
||||
populate: z
|
||||
.union([projectRelationsSchema, z.array(projectRelationsSchema)])
|
||||
|
@ -54,3 +59,9 @@ export const paginationAndPopulateProjectSchema = z.object({
|
|||
...paginationSchema.shape,
|
||||
...populateProjectSchema.shape
|
||||
})
|
||||
|
||||
export const listPublicProjectsQuerySchema = z.object({
|
||||
...paginationSchema.shape,
|
||||
...populateProjectSchema.shape,
|
||||
...filterPublicProjectSchema.shape
|
||||
})
|
||||
|
|
|
@ -73,7 +73,7 @@ export const deployments = pgTable(
|
|||
name: projectName().notNull(),
|
||||
|
||||
description: text().default('').notNull(),
|
||||
readme: text().default('').notNull(),
|
||||
readme: text(), // URL to uploaded markdown document
|
||||
iconUrl: text(),
|
||||
sourceUrl: text(),
|
||||
homepageUrl: text(),
|
||||
|
|
|
@ -85,6 +85,9 @@ export const projects = pgTable(
|
|||
// visible on the marketplace.
|
||||
private: boolean().default(true).notNull(),
|
||||
|
||||
// Admin-controlled tags for organizing and featuring on the marketplace
|
||||
tags: text().array(),
|
||||
|
||||
// TODO: allow for multiple aliases like vercel
|
||||
// alias: text(),
|
||||
|
||||
|
@ -172,6 +175,7 @@ export const projects = pgTable(
|
|||
index('project_teamId_idx').on(table.teamId),
|
||||
// index('project_alias_idx').on(table.alias),
|
||||
index('project_private_idx').on(table.private),
|
||||
index('project_tags_idx').on(table.tags),
|
||||
index('project_lastPublishedDeploymentId_idx').on(
|
||||
table.lastPublishedDeploymentId
|
||||
),
|
||||
|
@ -215,6 +219,7 @@ export const projectSelectBaseSchema = createSelectSchema(projects, {
|
|||
identifier: projectIdentifierSchema,
|
||||
name: agenticProjectConfigSchema.shape.name,
|
||||
slug: agenticProjectConfigSchema.shape.slug,
|
||||
tags: z.array(z.string()).optional(),
|
||||
lastPublishedDeploymentId: deploymentIdSchema.optional(),
|
||||
lastDeploymentId: deploymentIdSchema.optional(),
|
||||
|
||||
|
|
|
@ -8,11 +8,29 @@ import { LoadingIndicator } from '@/components/loading-indicator'
|
|||
import { PageContainer } from '@/components/page-container'
|
||||
import { PublicProject } from '@/components/public-project'
|
||||
import { SupplySideCTA } from '@/components/supply-side-cta'
|
||||
import { useInfiniteQuery } from '@/lib/query-client'
|
||||
import { useInfiniteQuery, useQuery } from '@/lib/query-client'
|
||||
|
||||
export function MarketplaceIndex() {
|
||||
const ctx = useAgentic()
|
||||
const limit = 10
|
||||
|
||||
const {
|
||||
data: featuredProjects,
|
||||
isLoading: isFeaturedProjectsLoading,
|
||||
isError: isFeaturedProjectsError
|
||||
} = useQuery({
|
||||
queryKey: ['featured-public-projects'],
|
||||
queryFn: () =>
|
||||
ctx!.api.listPublicProjects({
|
||||
populate: ['lastPublishedDeployment'],
|
||||
limit: 3,
|
||||
tag: 'featured',
|
||||
sortBy: 'createdAt',
|
||||
sort: 'asc'
|
||||
}),
|
||||
enabled: !!ctx
|
||||
})
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
|
@ -21,13 +39,14 @@ export function MarketplaceIndex() {
|
|||
fetchNextPage,
|
||||
isFetchingNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['projects'],
|
||||
queryKey: ['public-projects'],
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
ctx!.api
|
||||
.listPublicProjects({
|
||||
populate: ['lastPublishedDeployment'],
|
||||
offset: pageParam,
|
||||
limit
|
||||
limit,
|
||||
notTag: 'featured'
|
||||
})
|
||||
.then(async (projects) => {
|
||||
return {
|
||||
|
@ -55,7 +74,7 @@ export function MarketplaceIndex() {
|
|||
|
||||
return (
|
||||
<PageContainer>
|
||||
<section>
|
||||
<section className='flex flex-col gap-8'>
|
||||
<h1
|
||||
className='text-center text-balance leading-snug md:leading-none
|
||||
text-4xl font-extrabold'
|
||||
|
@ -63,17 +82,38 @@ export function MarketplaceIndex() {
|
|||
Marketplace
|
||||
</h1>
|
||||
|
||||
{!ctx || isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<div className='mt-8'>
|
||||
<h2 className='text-xl font-semibold mb-4'>Public Projects</h2>
|
||||
<div className='flex flex-col gap-16'>
|
||||
<div className=''>
|
||||
<h2 className='text-xl font-semibold mb-4'>Featured</h2>
|
||||
|
||||
{isFeaturedProjectsError ? (
|
||||
<p>Error fetching featured projects</p>
|
||||
) : isFeaturedProjectsLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : !featuredProjects?.length ? (
|
||||
<p>
|
||||
No projects found. This is likely an issue on Agentic's side.
|
||||
Please refresh or contact support.
|
||||
</p>
|
||||
) : (
|
||||
<div className='grid grid-cols grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3'>
|
||||
{featuredProjects.map((project) => (
|
||||
<PublicProject key={project.id} project={project} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className=''>
|
||||
<h2 className='text-xl font-semibold mb-4'>General</h2>
|
||||
|
||||
{isError ? (
|
||||
<p>Error fetching projects</p>
|
||||
) : isLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : !projects.length ? (
|
||||
<p>
|
||||
No projects found. This is likely an error on Agentic's side.
|
||||
No projects found. This is likely an issue on Agentic's side.
|
||||
Please refresh or contact support.
|
||||
</p>
|
||||
) : (
|
||||
|
@ -90,7 +130,7 @@ export function MarketplaceIndex() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA section */}
|
||||
|
@ -99,7 +139,7 @@ export function MarketplaceIndex() {
|
|||
Your API → Paid MCP, Instantly
|
||||
</h2>
|
||||
|
||||
<h5 className='text-center max-w-2xl'>
|
||||
<h5 className='text-center max-w-2xl bg-background/50 rounded-xl'>
|
||||
Run one command to turn any MCP server or OpenAPI service into a paid
|
||||
MCP product. With built-in support for every major LLM SDK and MCP
|
||||
client.
|
||||
|
|
|
@ -36,11 +36,9 @@ export function PublicProject({ project }: { project: Project }) {
|
|||
</div>
|
||||
|
||||
<div className='flex-1 flex flex-col gap-3 justify-between'>
|
||||
{deployment.description && (
|
||||
<p className='text-sm text-gray-700 line-clamp-4'>
|
||||
{deployment.description}
|
||||
</p>
|
||||
)}
|
||||
<p className='text-sm text-gray-700 line-clamp-4'>
|
||||
{deployment.description}
|
||||
</p>
|
||||
|
||||
{project.lastPublishedDeployment && (
|
||||
<div className='text-xs text-gray-500 flex gap-3 items-center justify-between'>
|
||||
|
|
|
@ -622,6 +622,7 @@ export interface components {
|
|||
/** @description Unique project slug. Must be ascii-only, lower-case, and kebab-case with no spaces between 1 and 256 characters. If not provided, it will be derived by slugifying `name`. */
|
||||
slug?: string;
|
||||
private: boolean;
|
||||
tags?: string[];
|
||||
/** @description User id (e.g. "user_tz4a98xxat96iws9zmbrgj3a") */
|
||||
userId: string;
|
||||
/** @description Team id (e.g. "team_tz4a98xxat96iws9zmbrgj3a") */
|
||||
|
@ -1364,6 +1365,8 @@ export interface operations {
|
|||
sort?: "asc" | "desc";
|
||||
sortBy?: "createdAt" | "updatedAt";
|
||||
populate?: ("user" | "team" | "lastPublishedDeployment" | "lastDeployment") | ("user" | "team" | "lastPublishedDeployment" | "lastDeployment")[];
|
||||
tag?: string;
|
||||
notTag?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
|
12
todo.md
12
todo.md
|
@ -18,13 +18,12 @@
|
|||
- docs: add notes about constraints on mcp origin servers (static tools)
|
||||
- **api keys should go beyond 1:1 consumers**
|
||||
- **currently not obvious how to get api key**
|
||||
- marketplace public project page
|
||||
- mcp inspector
|
||||
- **add support to example-usage for api keys**
|
||||
- add last published date somewhere
|
||||
- marketplace public project detail page
|
||||
- add breadcrumb nav: marketplace > @agentic > search
|
||||
- tool input/output schemas; move `$schema` to top
|
||||
- add last published date somewhere
|
||||
- tool input/output schemas; move `$schema` to the top
|
||||
- break out into a few subcomponents; some can be server components
|
||||
- mcp inspector
|
||||
- improve private project page
|
||||
- link to public page if published
|
||||
- list deployment versions
|
||||
|
@ -34,9 +33,6 @@
|
|||
- marketplace index page
|
||||
- add disclaimer about public beta
|
||||
- add search / sorting
|
||||
- **improve marketplace default sorting**
|
||||
- what's the best way to implement this?
|
||||
- add admin-based tags for main page layout (featured, etc)
|
||||
- replace render for api and/or add turbo for caching (too slow to deploy)
|
||||
- create slack + notifications
|
||||
- consider changing homepage hero CTA to include publishing
|
||||
|
|
Ładowanie…
Reference in New Issue