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