diff --git a/.cursor/hooks/state/continual-learning.json b/.cursor/hooks/state/continual-learning.json new file mode 100644 index 00000000..b3eb5b50 --- /dev/null +++ b/.cursor/hooks/state/continual-learning.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "lastRunAtMs": 0, + "turnsSinceLastRun": 1, + "lastTranscriptMtimeMs": null, + "lastProcessedGenerationId": "02f2727d-db77-45c6-bb2d-4163883b764f", + "trialStartedAtMs": null +} diff --git a/bun.lock b/bun.lock index d5232b78..0e51f3c5 100644 --- a/bun.lock +++ b/bun.lock @@ -56,7 +56,7 @@ "@uploadthing/react": "^7.3.3", "@vercel/speed-insights": "^1.3.1", "@webcontainer/api": "^1.6.1", - "ai": "^6.0.5", + "ai": "^6.0.146", "class-variance-authority": "^0.7.1", "claude": "^0.1.2", "client-only": "^0.0.1", @@ -125,7 +125,7 @@ "@ai-sdk/fireworks": ["@ai-sdk/fireworks@2.0.42", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AF8UxWqLRxpgAmlKs++v9WdjfnaPs5P+Vce2HUpDimX4eoA/t28fC6fihyFyh7AC5zb8IcxmhR9xQ7DVtmUT+A=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OlccjNYZ5+4FaNyvs0kb3N5H6U/QCKlKPTGsgUo8IZkqfMQu8ALI1XD6l/BCuTKto+OO9xUPObT/W7JhbqJ5nA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.88", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.22", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-AFoj7xdWAtCQcy0jJ235ENSakYM8D28qBX+rB+/rX4r8qe/LXgl0e5UivOqxAlIM5E9jnQdYxIPuj3XFtGk/yg=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GONwavgSWtcWO+t9+GpGK8l7nIYh+zNtCL/NYDSeHxHiw6ksQS9XMRWrZyE5NpJ0EXNxSAWCHIDmb1WvTqhq9Q=="], @@ -1267,7 +1267,7 @@ "@vercel/functions": ["@vercel/functions@2.2.13", "", { "dependencies": { "@vercel/oidc": "2.0.2" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-14ArBSIIcOBx9nrEgaJb4Bw+en1gl6eSoJWh8qjifLl5G3E4dRXCFOT8HP+w66vb9Wqyd1lAQBrmRhRwOj9X9A=="], - "@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], "@vercel/speed-insights": ["@vercel/speed-insights@1.3.1", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ=="], @@ -1319,7 +1319,7 @@ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "ai": ["ai@6.0.5", "", { "dependencies": { "@ai-sdk/gateway": "3.0.4", "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CKL3dDHedWskC6EY67LrULonZBU9vL+Bwa+xQEcprBhJfxpogntG3utjiAkYuy5ZQatyWk+SmWG8HLvcnhvbRg=="], + "ai": ["ai@6.0.146", "", { "dependencies": { "@ai-sdk/gateway": "3.0.88", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.22", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-70DE8k1rR0N3mXxyyfjYAx/FxRln/kQ5ym18lt1ys1eUklcPuoIXGbUBwdfCbmkt6YF3jCDZ5+OgkWieP/NGDw=="], "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -2721,9 +2721,9 @@ "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg=="], + "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KaykkuRBdF/ffpI5bwpL4aSCmO/99p8/ci+VeHwJO8tmvXtiVAb99QeyvvvXmL61e9Zrvv4GBGoajW19xdjkVQ=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.22", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg=="], @@ -3125,9 +3125,9 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg=="], + "ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KaykkuRBdF/ffpI5bwpL4aSCmO/99p8/ci+VeHwJO8tmvXtiVAb99QeyvvvXmL61e9Zrvv4GBGoajW19xdjkVQ=="], + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.22", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index d7dec618..95031672 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -18,6 +18,7 @@ import type * as oauth from "../oauth.js"; import type * as polar from "../polar.js"; import type * as projects from "../projects.js"; import type * as rateLimit from "../rateLimit.js"; +import type * as schemaProposals from "../schemaProposals.js"; import type * as subscriptions from "../subscriptions.js"; import type * as usage from "../usage.js"; import type * as webhooks from "../webhooks.js"; @@ -39,6 +40,7 @@ declare const fullApi: ApiFromModules<{ polar: typeof polar; projects: typeof projects; rateLimit: typeof rateLimit; + schemaProposals: typeof schemaProposals; subscriptions: typeof subscriptions; usage: typeof usage; webhooks: typeof webhooks; diff --git a/convex/messages.ts b/convex/messages.ts index d8c32f48..121d27ab 100644 --- a/convex/messages.ts +++ b/convex/messages.ts @@ -578,6 +578,8 @@ export const createFragmentForUser = mutation({ files: v.any(), metadata: v.optional(v.any()), framework: frameworkEnum, + hasBackend: v.optional(v.boolean()), + backendFiles: v.optional(v.any()), }, handler: async (ctx, args) => { const message = await ctx.db.get(args.messageId); @@ -600,6 +602,8 @@ export const createFragmentForUser = mutation({ files: args.files, metadata: args.metadata, framework: args.framework, + ...(args.hasBackend !== undefined && { hasBackend: args.hasBackend }), + ...(args.backendFiles !== undefined && { backendFiles: args.backendFiles }), updatedAt: now, }); return existingFragment._id; @@ -612,6 +616,8 @@ export const createFragmentForUser = mutation({ files: args.files, metadata: args.metadata, framework: args.framework, + ...(args.hasBackend !== undefined && { hasBackend: args.hasBackend }), + ...(args.backendFiles !== undefined && { backendFiles: args.backendFiles }), createdAt: now, updatedAt: now, }); diff --git a/convex/projects.ts b/convex/projects.ts index 6a88c339..4e95c98d 100644 --- a/convex/projects.ts +++ b/convex/projects.ts @@ -484,6 +484,24 @@ export const createForUser = mutation({ }, }); +export const setHasBackendForUser = mutation({ + args: { + userId: v.string(), + projectId: v.id("projects"), + hasBackend: v.boolean(), + }, + handler: async (ctx, args) => { + const project = await ctx.db.get(args.projectId); + if (!project || project.userId !== args.userId) { + throw new Error("Unauthorized"); + } + await ctx.db.patch(args.projectId, { + hasBackend: args.hasBackend, + updatedAt: Date.now(), + }); + }, +}); + /** * Internal: Create a project for a specific user (for use from actions/background jobs) */ diff --git a/convex/schema.ts b/convex/schema.ts index e5f20a3e..ce1f921e 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -81,6 +81,13 @@ export const subscriptionIntervalEnum = v.union( v.literal("yearly") ); +export const schemaProposalStatusEnum = v.union( + v.literal("PENDING"), + v.literal("APPROVED"), + v.literal("REJECTED"), + v.literal("IMPLEMENTED") +); + const polarCustomers = defineTable({ userId: v.string(), polarCustomerId: v.string(), @@ -95,6 +102,7 @@ export default defineSchema({ userId: v.string(), framework: frameworkEnum, modelPreference: v.optional(v.string()), + hasBackend: v.optional(v.boolean()), createdAt: v.optional(v.number()), updatedAt: v.optional(v.number()), }) @@ -121,6 +129,8 @@ export default defineSchema({ files: v.any(), metadata: v.optional(v.any()), framework: frameworkEnum, + hasBackend: v.optional(v.boolean()), + backendFiles: v.optional(v.any()), createdAt: v.optional(v.number()), updatedAt: v.optional(v.number()), }) @@ -135,6 +145,29 @@ export default defineSchema({ }) .index("by_projectId", ["projectId"]), + schemaProposals: defineTable({ + projectId: v.id("projects"), + messageId: v.id("messages"), + userId: v.string(), + proposal: v.string(), + status: schemaProposalStatusEnum, + parsedTables: v.optional(v.array(v.object({ + name: v.string(), + purpose: v.string(), + fields: v.array(v.string()), + indexes: v.array(v.string()), + }))), + parsedRelationships: v.optional(v.array(v.string())), + approvedAt: v.optional(v.number()), + implementedAt: v.optional(v.number()), + createdAt: v.number(), + updatedAt: v.number(), + }) + .index("by_projectId", ["projectId"]) + .index("by_projectId_status", ["projectId", "status"]) + .index("by_messageId", ["messageId"]) + .index("by_userId", ["userId"]), + attachments: defineTable({ type: attachmentTypeEnum, url: v.string(), @@ -264,6 +297,7 @@ export default defineSchema({ claimedBy: v.optional(v.string()), messageId: v.optional(v.id("messages")), fragmentId: v.optional(v.id("fragments")), + schemaProposalId: v.optional(v.id("schemaProposals")), error: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), diff --git a/convex/schemaProposals.ts b/convex/schemaProposals.ts new file mode 100644 index 00000000..799668cb --- /dev/null +++ b/convex/schemaProposals.ts @@ -0,0 +1,66 @@ +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; +import { schemaProposalStatusEnum } from "./schema"; + +export const createForUser = mutation({ + args: { + userId: v.string(), + projectId: v.id("projects"), + messageId: v.id("messages"), + proposal: v.string(), + parsedTables: v.optional( + v.array( + v.object({ + name: v.string(), + purpose: v.string(), + fields: v.array(v.string()), + indexes: v.array(v.string()), + }) + ) + ), + parsedRelationships: v.optional(v.array(v.string())), + status: schemaProposalStatusEnum, + }, + handler: async (ctx, args) => { + const project = await ctx.db.get(args.projectId); + if (!project || project.userId !== args.userId) { + throw new Error("Unauthorized"); + } + const message = await ctx.db.get(args.messageId); + if (!message || message.projectId !== args.projectId) { + throw new Error("Message not found"); + } + const now = Date.now(); + return await ctx.db.insert("schemaProposals", { + projectId: args.projectId, + messageId: args.messageId, + userId: args.userId, + proposal: args.proposal, + status: args.status, + parsedTables: args.parsedTables, + parsedRelationships: args.parsedRelationships, + createdAt: now, + updatedAt: now, + }); + }, +}); + +export const markImplementedForUser = mutation({ + args: { + userId: v.string(), + schemaProposalId: v.id("schemaProposals"), + }, + handler: async (ctx, args) => { + const row = await ctx.db.get(args.schemaProposalId); + if (!row || row.userId !== args.userId) { + throw new Error("Unauthorized"); + } + const now = Date.now(); + await ctx.db.patch(args.schemaProposalId, { + status: "IMPLEMENTED", + approvedAt: row.approvedAt ?? now, + implementedAt: now, + updatedAt: now, + }); + }, +}); diff --git a/package.json b/package.json index 0ebb16ab..b404039a 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@uploadthing/react": "^7.3.3", "@vercel/speed-insights": "^1.3.1", "@webcontainer/api": "^1.6.1", - "ai": "^6.0.5", + "ai": "^6.0.146", "class-variance-authority": "^0.7.1", "claude": "^0.1.2", "client-only": "^0.0.1", diff --git a/src/agents/backend-agent.ts b/src/agents/backend-agent.ts new file mode 100644 index 00000000..8504881b --- /dev/null +++ b/src/agents/backend-agent.ts @@ -0,0 +1,107 @@ +import { generateText } from "ai"; +import { openrouter } from "./client"; +import { CONVEX_BACKEND_PROMPT } from "@/prompts/backend/convex-backend"; + +const BACKEND_MODEL = "moonshotai/kimi-k2.5:nitro"; + +export interface BackendAgentResult { + files: Record; + success: boolean; + error?: string; + summary?: string; +} + +export async function runBackendImplementerAgent( + userPrompt: string, + schemaProposal: string, + plan?: string +): Promise { + console.log("[BACKEND] Starting implementation..."); + + try { + const augmentedPrompt = [ + "## User Request", + userPrompt, + "", + "## Approved Schema Design", + schemaProposal, + "", + plan ? `## Implementation Plan\n${plan}\n` : "", + "## Your Task", + "Generate the complete Convex backend implementation based on the approved schema.", + "Create all necessary files with their full content.", + "", + "Output format:", + '1. First, output all file contents using tags', + "2. Each file should contain the complete, production-ready code", + "3. End with a describing what was created", + "", + "Example:", + '', + "// schema content here", + "", + '', + "// queries content here", + "", + "", + "", + "Created Convex backend with schema and CRUD operations for tasks", + "", + ].join("\n"); + + const { text } = await generateText({ + model: openrouter(BACKEND_MODEL), + system: CONVEX_BACKEND_PROMPT, + prompt: augmentedPrompt, + temperature: 0.2, + maxOutputTokens: 8192, + }); + + const files = parseGeneratedFiles(text); + + const summary = text.includes("") + ? text.match(/([\s\S]*?)<\/task_summary>/)?.[1]?.trim() + : "Generated Convex backend files"; + + if (Object.keys(files).length === 0) { + return { + files, + success: false, + error: "No files were generated", + summary, + }; + } + + console.log("[BACKEND] Completed successfully"); + + return { + files, + success: true, + summary, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("[BACKEND] Error:", errorMessage); + + return { + files: {}, + success: false, + error: errorMessage, + }; + } +} + +function parseGeneratedFiles(text: string): Record { + const files: Record = {}; + + const fileRegex = /([\s\S]*?)<\/zapdev_file>/g; + let match; + + while ((match = fileRegex.exec(text)) !== null) { + const path = match[1]; + const content = match[2].trim(); + files[path] = content; + } + + return files; +} diff --git a/src/agents/index.ts b/src/agents/index.ts index 33ee74e0..e74d10c8 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -11,3 +11,16 @@ export { } from "./types"; export { type ToolContext } from "./tools"; export { type StreamEvent } from "./code-agent"; + +export { + runSchemaProposalAgent, + parseSchemaProposal, + type SchemaProposalResult, +} from "./schema-proposal-agent"; + +export { + runBackendImplementerAgent, + type BackendAgentResult, +} from "./backend-agent"; + +export { wantsConvexBackend } from "./wants-backend"; diff --git a/src/agents/schema-proposal-agent.ts b/src/agents/schema-proposal-agent.ts new file mode 100644 index 00000000..dcc5e045 --- /dev/null +++ b/src/agents/schema-proposal-agent.ts @@ -0,0 +1,150 @@ +import { generateText } from "ai"; +import { openrouter } from "./client"; +import { SCHEMA_PROPOSAL_PROMPT } from "@/prompts/backend/schema-proposal"; + +const SCHEMA_PROPOSAL_MODEL = "moonshotai/kimi-k2.5:nitro"; + +export interface SchemaProposalResult { + schemaProposal: string; + success: boolean; + error?: string; +} + +export async function runSchemaProposalAgent( + userPrompt: string, + plan?: string, + research?: string +): Promise { + console.log("[SCHEMA PROPOSAL] Starting analysis..."); + + try { + const contextBlocks: string[] = []; + + if (plan) { + contextBlocks.push(`## Implementation Plan\n${plan}\n`); + } + + if (research) { + contextBlocks.push(`## Research Findings\n${research}\n`); + } + + const augmentedPrompt = [ + "## User Request", + userPrompt, + "", + ...contextBlocks, + "## Your Task", + "Based on the above requirements, design a complete Convex database schema.", + "Focus on the data model - what tables are needed, their fields, relationships, and indexes.", + "Output ONLY the schema_proposal block in the format specified in your instructions.", + ].join("\n"); + + const { text } = await generateText({ + model: openrouter(SCHEMA_PROPOSAL_MODEL), + system: SCHEMA_PROPOSAL_PROMPT, + prompt: augmentedPrompt, + temperature: 0.3, + maxOutputTokens: 4096, + }); + + if (!text.includes("")) { + console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag"); + return { + schemaProposal: text, + success: false, + error: "Invalid schema proposal format", + }; + } + + console.log("[SCHEMA PROPOSAL] Completed successfully"); + + return { + schemaProposal: text, + success: true, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error("[SCHEMA PROPOSAL] Error:", errorMessage); + + return { + schemaProposal: "", + success: false, + error: errorMessage, + }; + } +} + +export function parseSchemaProposal(proposal: string): { + overview: string; + tables: Array<{ + name: string; + purpose: string; + fields: string[]; + indexes: string[]; + }>; + relationships: string[]; +} { + const result = { + overview: "", + tables: [] as Array<{ + name: string; + purpose: string; + fields: string[]; + indexes: string[]; + }>, + relationships: [] as string[], + }; + + const match = proposal.match(/([\s\S]*?)<\/schema_proposal>/); + if (!match) { + return result; + } + + const content = match[1]; + + const overviewMatch = content.match(/## Overview\n([\s\S]*?)(?=##|$)/); + if (overviewMatch) { + result.overview = overviewMatch[1].trim(); + } + + const tableMatches = content.matchAll( + /### (\w+)\nPurpose: ([\s\S]*?)Fields:([\s\S]*?)Indexes:([\s\S]*?)(?=###|##|$)/g + ); + + for (const tableMatch of tableMatches) { + const tableName = tableMatch[1]; + const purpose = tableMatch[2].trim(); + const fieldsSection = tableMatch[3]; + const indexesSection = tableMatch[4]; + + const fields: string[] = []; + const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g); + for (const fieldMatch of fieldMatches) { + fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`); + } + + const indexes: string[] = []; + const indexMatches = indexesSection.matchAll(/- `([^`]+)`: \[([^\]]+)\]/g); + for (const indexMatch of indexMatches) { + indexes.push(`${indexMatch[1]} (${indexMatch[2]})`); + } + + result.tables.push({ + name: tableName, + purpose, + fields, + indexes, + }); + } + + const relationshipsMatch = content.match(/## Relationships\n([\s\S]*?)(?=##|$)/); + if (relationshipsMatch) { + const relationshipsContent = relationshipsMatch[1]; + const relMatches = relationshipsContent.matchAll(/- ([^\n]+)/g); + for (const relMatch of relMatches) { + result.relationships.push(relMatch[1].trim()); + } + } + + return result; +} diff --git a/src/agents/wants-backend.ts b/src/agents/wants-backend.ts new file mode 100644 index 00000000..6f0309c2 --- /dev/null +++ b/src/agents/wants-backend.ts @@ -0,0 +1,6 @@ +const BACKEND_INTENT = + /\b(convex|database|db\b|backend|persistent|persist(ed|ence)?|real[\s-]?time|authentication|authorize|login|signup|sign[\s-]?up|user\s+(accounts?|profiles?)|crud|rest\s+api|graphql|mutations?|queries?|schema\b|postgres|sqlite|mongodb|supabase|prisma|drizzle|kv\s+store|sessions?)\b/i; + +export function wantsConvexBackend(prompt: string): boolean { + return BACKEND_INTENT.test(prompt); +} diff --git a/src/app/api/agent/run/route.ts b/src/app/api/agent/run/route.ts index 211ce4fd..a812d9f3 100644 --- a/src/app/api/agent/run/route.ts +++ b/src/app/api/agent/run/route.ts @@ -11,10 +11,11 @@ export async function POST(request: NextRequest) { } const body = await request.json(); - const { projectId, value, model } = body as { + const { projectId, value, model, messageId } = body as { projectId?: unknown; value?: unknown; model?: unknown; + messageId?: unknown; }; if ( @@ -36,6 +37,10 @@ export async function POST(request: NextRequest) { value, userId, model: typeof model === "string" && model.trim().length > 0 ? model : undefined, + messageId: + typeof messageId === "string" && messageId.trim().length > 0 + ? messageId + : undefined, }, }); diff --git a/src/inngest/functions.ts b/src/inngest/functions.ts index 2514cf76..1d80c59d 100644 --- a/src/inngest/functions.ts +++ b/src/inngest/functions.ts @@ -14,9 +14,20 @@ import { ConvexHttpClient } from "convex/browser"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; -import { FRAGMENT_TITLE_PROMPT, PROMPT, RESPONSE_PROMPT } from "@/prompt"; +import { + FRAGMENT_TITLE_PROMPT, + FULLSTACK_PROMPT, + PROMPT, + RESPONSE_PROMPT, +} from "@/prompt"; import { PLANNING_AGENT_PROMPT } from "@/prompts/planning"; import { RESEARCH_AGENT_PROMPT } from "@/prompts/research"; +import { + parseSchemaProposal, + runBackendImplementerAgent, + runSchemaProposalAgent, + wantsConvexBackend, +} from "@/agents"; import { selectModelForTask } from "@/agents/types"; import { openrouter } from "@/agents/client"; @@ -255,8 +266,99 @@ export const codeAgentFunction = inngest.createFunction( ) : ""; + const projectId = event.data.projectId as Id<"projects">; + const userId = event.data.userId as string; + + const project = await step.run("get-project-for-agent", async () => { + const convex = getConvexClient(); + return await convex.query(api.projects.getForUser, { userId, projectId }); + }); + + let backendPreload: Record = {}; + let schemaProposalText: string | undefined; + + const needsBackendBootstrap = + Boolean(project && !project.hasBackend && wantsConvexBackend(userPrompt)); + + if (needsBackendBootstrap) { + const triggerMessageId = await step.run("resolve-trigger-message", async () => { + const explicit = event.data.messageId as string | undefined; + if (explicit && explicit.length > 0) { + return explicit as Id<"messages">; + } + const convex = getConvexClient(); + const messages = await convex.query(api.messages.listForUser, { + userId, + projectId, + }); + const lastUser = [...messages].reverse().find((m) => m.role === "USER"); + return lastUser?._id ?? null; + }); + + if (triggerMessageId) { + const schemaProposalResult = await step.run("schema-proposal-agent", () => + runSchemaProposalAgent(userPrompt, plan || undefined, research || undefined) + ); + + if (schemaProposalResult.success) { + schemaProposalText = schemaProposalResult.schemaProposal; + const parsed = parseSchemaProposal(schemaProposalResult.schemaProposal); + const schemaProposalRowId = await step.run("persist-schema-proposal", async () => { + const convex = getConvexClient(); + return await convex.mutation(api.schemaProposals.createForUser, { + userId, + projectId, + messageId: triggerMessageId, + proposal: schemaProposalResult.schemaProposal, + parsedTables: + parsed.tables.length > 0 + ? parsed.tables.map((t) => ({ + name: t.name, + purpose: t.purpose, + fields: t.fields, + indexes: t.indexes, + })) + : undefined, + parsedRelationships: + parsed.relationships.length > 0 ? parsed.relationships : undefined, + status: "PENDING", + }); + }); + + const backendResult = await step.run("backend-implementer", () => + runBackendImplementerAgent( + userPrompt, + schemaProposalResult.schemaProposal, + plan || undefined + ) + ); + + if (backendResult.success && Object.keys(backendResult.files).length > 0) { + backendPreload = backendResult.files; + await step.run("finalize-backend-bootstrap", async () => { + const convex = getConvexClient(); + await convex.mutation(api.schemaProposals.markImplementedForUser, { + userId, + schemaProposalId: schemaProposalRowId, + }); + await convex.mutation(api.projects.setHasBackendForUser, { + userId, + projectId, + hasBackend: true, + }); + }); + } + } + } + } + + const useFullstackPrompt = + Boolean(project?.hasBackend) || + wantsConvexBackend(userPrompt) || + Object.keys(backendPreload).length > 0; + const state = createState( - { summary: "", files: {} }, + { summary: "", files: { ...backendPreload } }, { messages: previousMessages } ); @@ -273,18 +375,36 @@ export const codeAgentFunction = inngest.createFunction( ? "\n\nUse the implementation plan and research findings above as your blueprint. Follow the plan precisely and apply the research insights to ensure accuracy." : ""; - const augmentedPrompt = `${userPrompt}${planBlock}${researchBlock}${contextNote}`; + const convexBlock = + schemaProposalText || Object.keys(backendPreload).length > 0 + ? `\n\n\n${ + schemaProposalText + ? `Schema design (approved):\n${schemaProposalText}\n\n` + : "" + }${ + Object.keys(backendPreload).length > 0 + ? `Convex backend files are already present in the workspace: ${Object.keys( + backendPreload + ).join(", ")}. Extend them only if needed; do not discard them.\n` + : "" + }` + : ""; + + const augmentedPrompt = `${userPrompt}${planBlock}${researchBlock}${convexBlock}${contextNote}`; // ── Step 5: Code Agent (user-selected model) ───────────────────────────── const selectedModel = getModelForAgent( event.data.model as string | undefined, userPrompt ); - console.log(`[CODING] Starting with ${selectedModel}...`); + console.log( + `[CODING] Starting with ${selectedModel} (fullstack=${useFullstackPrompt})...` + ); const codeAgent = createAgent({ name: "code-agent", description: "An expert coding agent", + system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT, system: PROMPT, tool_choice: "auto", model: openai({ @@ -434,8 +554,6 @@ export const codeAgentFunction = inngest.createFunction( await step.run("save-result", async () => { const convex = getConvexClient(); - const userId = event.data.userId as string; - const projectId = event.data.projectId as Id<"projects">; const messageId = await convex.mutation(api.messages.createForUser, { userId, @@ -445,6 +563,9 @@ export const codeAgentFunction = inngest.createFunction( type: "RESULT", }); + const backendKeys = Object.keys(backendPreload); + const hasBackendFiles = backendKeys.length > 0; + await convex.mutation(api.messages.createFragmentForUser, { userId, messageId: messageId as Id<"messages">, @@ -452,6 +573,10 @@ export const codeAgentFunction = inngest.createFunction( title: parseAgentOutput(fragmentTitleOutput), files: result.state.data.files, framework: "NEXTJS", + ...(hasBackendFiles && { + hasBackend: true, + backendFiles: backendPreload, + }), }); return messageId; diff --git a/src/modules/home/ui/components/project-form.tsx b/src/modules/home/ui/components/project-form.tsx index a7a89be7..78e45ae8 100644 --- a/src/modules/home/ui/components/project-form.tsx +++ b/src/modules/home/ui/components/project-form.tsx @@ -85,6 +85,7 @@ export const ProjectForm = () => { projectId: result.id, value: result.value, model: selectedModel, + messageId: result.messageId, }), }); diff --git a/src/modules/projects/ui/components/message-form.tsx b/src/modules/projects/ui/components/message-form.tsx index 0cf836f0..440406b2 100644 --- a/src/modules/projects/ui/components/message-form.tsx +++ b/src/modules/projects/ui/components/message-form.tsx @@ -126,6 +126,7 @@ export const MessageForm = ({ projectId: result.projectId, value: result.value, model: selectedModel, + messageId: result.messageId, }), }); diff --git a/src/prompt.ts b/src/prompt.ts index b3dd914a..1f73888b 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -6,3 +6,9 @@ export { VUE_PROMPT } from "./prompts/vue"; export { SVELTE_PROMPT } from "./prompts/svelte"; export { FRAMEWORK_SELECTOR_PROMPT } from "./prompts/framework-selector"; export { NEXTJS_PROMPT as PROMPT } from "./prompts/nextjs"; + +export { + SCHEMA_PROPOSAL_PROMPT, + CONVEX_BACKEND_PROMPT, + FULLSTACK_PROMPT, +} from "./prompts/backend"; diff --git a/src/prompts/backend/convex-backend.ts b/src/prompts/backend/convex-backend.ts new file mode 100644 index 00000000..3094b996 --- /dev/null +++ b/src/prompts/backend/convex-backend.ts @@ -0,0 +1,199 @@ +export const CONVEX_BACKEND_PROMPT = ` +You are a senior Convex backend engineer specializing in real-time database operations. + +## Your Task +Generate complete, production-ready Convex backend code based on an approved schema design. You will create the schema, queries, mutations, and actions. + +## Convex Rules (CRITICAL - NEVER VIOLATE) + +### Function Syntax +ALWAYS use the new function syntax: +\`\`\`typescript +import { query, mutation, action, internalQuery, internalMutation, internalAction } from "./_generated/server"; +import { v } from "convex/values"; +import { api, internal } from "./_generated/api"; + +export const myFunction = query({ + args: { field: v.string() }, + handler: async (ctx, args) => { + // implementation + }, +}); +\`\`\` + +### Validators +ALWAYS include argument validators for ALL functions: +- \`v.string()\` - strings +- \`v.number()\` - numbers +- \`v.boolean()\` - booleans +- \`v.id("tableName")\` - document IDs +- \`v.optional(v.type())\` - optional fields +- \`v.array(v.type())\` - arrays +- \`v.object({...})\` - objects + +### Query Guidelines +- NEVER use .filter() - use .withIndex() with proper indexes +- Use .unique() for single document lookup +- Use .first() or .take(n) for limited results +- Use .collect() only when you need ALL results +- Paginate with .paginate() for large datasets + +### Mutation Guidelines +- Use ctx.db.insert("table", data) to create +- Use ctx.db.patch("table", id, updates) for partial updates +- Use ctx.db.replace("table", id, data) for full replacement +- Use ctx.db.delete(id) for deletion + +### Actions Guidelines +- Actions run in Node.js runtime +- Add "use node"; at top of action files +- NEVER use ctx.db inside actions - use ctx.runQuery/ctx.runMutation +- Use actions for external APIs, long-running operations + +### Internal Functions +- Use internalQuery/internalMutation/internalAction for private functions +- Import via internal.table.function from "./_generated/api" +- These can only be called by other Convex functions + +## File Structure + +Generate files in this structure: + +\`\`\` +convex/ + schema.ts - Schema definition with all tables + [feature]/ + queries.ts - Public query functions + mutations.ts - Public mutation functions + actions.ts - Action functions (if needed) +\`\`\` + +## Required Patterns + +### 1. User Context +Always get the current user for user-scoped operations: +\`\`\`typescript +import { query } from "./_generated/server"; + +export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) return null; + return await ctx.db + .query("users") + .withIndex("by_clerkId", (q) => q.eq("clerkId", identity.subject)) + .unique(); + }, +}); +\`\`\` + +### 2. Authorization +Check ownership before mutations: +\`\`\`typescript +export const updateDocument = mutation({ + args: { id: v.id("documents"), updates: v.object({ title: v.optional(v.string()) }) }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + const doc = await ctx.db.get(args.id); + if (!doc) throw new Error("Document not found"); + if (doc.userId !== identity.subject) throw new Error("Not authorized"); + + await ctx.db.patch(args.id, args.updates); + }, +}); +\`\`\` + +### 3. Query with Pagination +\`\`\`typescript +import { paginationOptsValidator } from "convex/server"; + +export const listUserDocuments = query({ + args: { paginationOpts: paginationOptsValidator }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + return await ctx.db + .query("documents") + .withIndex("by_userId", (q) => q.eq("userId", identity.subject)) + .order("desc") + .paginate(args.paginationOpts); + }, +}); +\`\`\` + +### 4. Scheduled Actions +\`\`\`typescript +// In mutations.ts +export const createTaskWithReminder = mutation({ + args: { + title: v.string(), + dueDate: v.number(), + }, + handler: async (ctx, args) => { + const taskId = await ctx.db.insert("tasks", { + title: args.title, + dueDate: args.dueDate, + status: "pending", + }); + + // Schedule a reminder + await ctx.scheduler.runAt(args.dueDate - 3600000, internal.tasks.sendReminder, { + taskId, + }); + + return taskId; + }, +}); + +// In actions.ts +"use node"; +import { action, internalAction } from "./_generated/server"; + +export const sendReminder = internalAction({ + args: { taskId: v.id("tasks") }, + handler: async (ctx, args) => { + // Send email/push notification + await fetch("https://api.notifications.com/send", {...}); + }, +}); +\`\`\` + +## Environment Variables +Access in actions only: +\`\`\`typescript +"use node"; +import { action } from "./_generated/server"; + +export const myAction = action({ + args: {}, + handler: async (ctx) => { + const apiKey = process.env.MY_API_KEY; + // use apiKey... + }, +}); +\`\`\` + +## Complete Example + +Given a schema with users, projects, and tasks, generate: + +1. convex/schema.ts - Complete schema +2. convex/projects/queries.ts - getProject, listUserProjects +3. convex/projects/mutations.ts - createProject, updateProject, deleteProject +4. convex/tasks/queries.ts - getTask, listProjectTasks, listUserTasks +5. convex/tasks/mutations.ts - createTask, updateTask, deleteTask, toggleTaskStatus + +Each file should be complete, typed, and follow all Convex rules. + +## Output + +Use createOrUpdateFiles to write all files. After all files are created, output: + + +Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling. + +`; diff --git a/src/prompts/backend/fullstack.ts b/src/prompts/backend/fullstack.ts new file mode 100644 index 00000000..730b8c2c --- /dev/null +++ b/src/prompts/backend/fullstack.ts @@ -0,0 +1,284 @@ +import { SHARED_RULES, DESIGNER_RULES } from "../shared"; + +export const FULLSTACK_PROMPT = ` +You are a senior full-stack engineer building complete applications with Next.js 15 frontend and Convex backend. + +${SHARED_RULES} + +## Tech Stack + +### Frontend +- Next.js 15.3.3 with App Router +- React 19 +- TypeScript (strict) +- Tailwind CSS v4 +- Shadcn/ui components (pre-installed) + +### Backend +- Convex real-time database +- Server-side functions (queries, mutations, actions) +- File-based routing in convex/ directory + +## Architecture Overview + +You are generating a COMPLETE full-stack application. The flow is: + +1. Frontend (Next.js) → User interface with Shadcn components +2. Backend (Convex) → Real-time data with queries/mutations +3. Integration → React hooks connect frontend to backend + +## File Structure + +\`\`\` +app/ + page.tsx - Main page component + layout.tsx - Root layout + globals.css - Global styles (Tailwind) + components/ + [Component].tsx - React components + +convex/ + schema.ts - Database schema + [feature]/ + queries.ts - Query functions + mutations.ts - Mutation functions + +components/ui/ - Shadcn components (pre-installed) + button.tsx + card.tsx + input.tsx + ... + +lib/ + utils.ts - Utility functions (cn, etc.) + convex.ts - Convex client setup +\`\`\` + +## Convex Client Setup + +Generate lib/convex.ts: +\`\`\`typescript +import { ConvexReactClient } from "convex/react"; + +export const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); +\`\`\` + +## Frontend-Backend Integration + +### 1. Query Data (Frontend) +\`\`\`typescript +"use client"; +import { useQuery } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function TaskList() { + const tasks = useQuery(api.tasks.listUserTasks); + + if (tasks === undefined) return
Loading...
; + + return ( +
    + {tasks.map((task) => ( +
  • {task.title}
  • + ))} +
+ ); +} +\`\`\` + +### 2. Mutate Data (Frontend) +\`\`\`typescript +"use client"; +import { useMutation } from "convex/react"; +import { api } from "../convex/_generated/api"; + +export function CreateTaskForm() { + const createTask = useMutation(api.tasks.createTask); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await createTask({ title: "New Task", projectId: "..." }); + }; + + return
...
; +} +\`\`\` + +### 3. Real-time Subscriptions +Convex queries automatically re-render when data changes: +\`\`\`typescript +const messages = useQuery(api.messages.listByChannel, { channelId }); +// Automatically updates when new messages are added! +\`\`\` + +## Convex Backend Rules (CRITICAL) + +### Schema Definition +\`\`\`typescript +// convex/schema.ts +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + users: defineTable({ + clerkId: v.string(), + email: v.string(), + name: v.optional(v.string()), + }).index("by_clerkId", ["clerkId"]), + + tasks: defineTable({ + title: v.string(), + status: v.union(v.literal("todo"), v.literal("done")), + userId: v.id("users"), + }).index("by_userId", ["userId"]), +}); +\`\`\` + +### Query Functions +\`\`\`typescript +// convex/tasks/queries.ts +import { query } from "../_generated/server"; +import { v } from "convex/values"; + +export const listUserTasks = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) return []; + + return await ctx.db + .query("tasks") + .withIndex("by_userId", (q) => q.eq("userId", identity.subject)) + .collect(); + }, +}); +\`\`\` + +### Mutation Functions +\`\`\`typescript +// convex/tasks/mutations.ts +import { mutation } from "../_generated/server"; +import { v } from "convex/values"; + +export const createTask = mutation({ + args: { + title: v.string(), + status: v.optional(v.union(v.literal("todo"), v.literal("done"))), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Not authenticated"); + + return await ctx.db.insert("tasks", { + title: args.title, + status: args.status ?? "todo", + userId: identity.subject, + }); + }, +}); +\`\`\` + +## Critical Rules + +1. **ALWAYS use "use client"** for components using hooks (useQuery, useMutation) +2. **ALWAYS validate args** in Convex functions with v.* validators +3. **NEVER use .filter()** in Convex - use .withIndex() instead +4. **Check authentication** in mutations: const identity = await ctx.auth.getUserIdentity() +5. **Frontend imports**: import { api } from "@/convex/_generated/api" +6. **Type safety**: Use generated types from convex/_generated/api and convex/_generated/dataModel + +## Layout Requirements + +Wrap your app with ConvexProvider in app/layout.tsx: +\`\`\`typescript +import { ConvexProvider, ConvexReactClient } from "convex/react"; +import { api } from "@/convex/_generated/api"; + +const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +\`\`\` + +${DESIGNER_RULES} + +## Workflow + +1. **First**: Generate convex/schema.ts with all tables and indexes +2. **Second**: Generate convex/[feature]/queries.ts and mutations.ts +3. **Third**: Generate lib/convex.ts for client setup +4. **Fourth**: Generate app/layout.tsx with ConvexProvider +5. **Fifth**: Generate app/page.tsx with UI components +6. **Sixth**: Generate any additional components in app/components/ + +All files in ONE createOrUpdateFiles call. Then run npm run build to validate. + +## Real-time Features + +Convex provides automatic real-time updates. Build UI that reflects this: +- Show optimistic updates +- Handle loading states gracefully +- Display live changes as they happen +- Use loading skeletons from Shadcn + +## Example Component Pattern + +\`\`\`typescript +"use client"; + +import { useQuery, useMutation } from "convex/react"; +import { api } from "@/convex/_generated/api"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; + +export function TaskManager() { + const tasks = useQuery(api.tasks.listUserTasks); + const createTask = useMutation(api.tasks.createTask); + const [newTaskTitle, setNewTaskTitle] = useState(""); + + const handleCreate = async () => { + if (!newTaskTitle.trim()) return; + await createTask({ title: newTaskTitle }); + setNewTaskTitle(""); + }; + + if (tasks === undefined) { + return
Loading tasks...
; + } + + return ( + + + My Tasks + + +
+ setNewTaskTitle(e.target.value)} + placeholder="New task..." + /> + +
+
    + {tasks.map((task) => ( +
  • + {task.title} +
  • + ))} +
+
+
+ ); +} +\`\`\` +`; diff --git a/src/prompts/backend/index.ts b/src/prompts/backend/index.ts new file mode 100644 index 00000000..445c0e0d --- /dev/null +++ b/src/prompts/backend/index.ts @@ -0,0 +1,3 @@ +export { SCHEMA_PROPOSAL_PROMPT } from "./schema-proposal"; +export { CONVEX_BACKEND_PROMPT } from "./convex-backend"; +export { FULLSTACK_PROMPT } from "./fullstack"; diff --git a/src/prompts/backend/schema-proposal.ts b/src/prompts/backend/schema-proposal.ts new file mode 100644 index 00000000..a17df13a --- /dev/null +++ b/src/prompts/backend/schema-proposal.ts @@ -0,0 +1,146 @@ +export const SCHEMA_PROPOSAL_PROMPT = ` +You are a database schema architect specializing in Convex (the real-time backend platform). + +## Your Task +Analyze the user's application requirements and propose a complete Convex database schema. This schema will be reviewed by the user before implementation. + +## Output Format + +You MUST output a structured schema proposal in this exact format: + + +## Overview +Brief description of the data model and key relationships. + +## Tables + +### [tableName] +Purpose: What this table stores and its role in the application. + +Fields: +- \`fieldName\`: type - description (e.g., \`userId\`: v.id("users") - reference to the author) +- \`fieldName\`: type - description + +Indexes: +- \`by_fieldName\`: ["fieldName"] - query pattern this supports +- \`by_fieldName_and_other\`: ["fieldName", "otherField"] - compound index + +### [tableName2] +... + +## Relationships +- TableA → TableB: description (e.g., "One-to-many: A user has many posts") +- TableB → TableC: description + +## Security Notes +- Which data should be public vs private +- Any sensitive fields requiring special handling + +## Generated Files +List the files that will be created: +- convex/schema.ts - The schema definition +- convex/[table]/queries.ts - Query functions for this table +- convex/[table]/mutations.ts - Mutation functions for this table + + +## Schema Design Guidelines + +### Field Types (Convex validators) +- \`v.string()\` - for text, IDs, URLs +- \`v.number()\` - for integers and floats +- \`v.boolean()\` - true/false +- \`v.id("tableName")\` - foreign key references +- \`v.optional(v.type())\` - nullable fields +- \`v.array(v.type())\` - arrays +- \`v.object({...})\` - nested objects +- \`v.record(v.string(), v.type())\` - dynamic key-value maps + +### Indexing Best Practices +- ALWAYS create indexes for fields used in .withIndex() queries +- Index names should be descriptive: \`by_author\`, \`by_userId_and_createdAt\` +- Include all index fields in the name: \`by_field1_and_field2\` + +### Common Patterns +1. **Timestamps**: Convex auto-adds \`_creationTime\`, don't add createdAt manually +2. **Ownership**: Use \`userId: v.id("users")\` for user-owned data +3. **Soft Deletes**: Use \`isDeleted: v.optional(v.boolean())\` instead of hard deletes +4. **Ordering**: Use \`order: v.number()\` or rely on \`_creationTime\` for ordering + +### Example Schema + + +## Overview +A task management app where users can create projects and tasks within those projects. + +## Tables + +### users +Purpose: Stores user profiles linked to Clerk authentication. + +Fields: +- \`clerkId\`: v.string() - Clerk user ID for authentication +- \`email\`: v.string() - User's email address +- \`name\`: v.optional(v.string()) - Display name + +Indexes: +- \`by_clerkId\`: ["clerkId"] - lookup by Clerk ID + +### projects +Purpose: Top-level containers for organizing tasks. + +Fields: +- \`name\`: v.string() - Project name +- \`description\`: v.optional(v.string()) - Project description +- \`userId\`: v.id("users") - Project owner +- \`isArchived\`: v.optional(v.boolean()) - Soft delete flag + +Indexes: +- \`by_userId\`: ["userId"] - list user's projects +- \`by_userId_and_isArchived\`: ["userId", "isArchived"] - filter archived + +### tasks +Purpose: Individual tasks belonging to a project. + +Fields: +- \`title\`: v.string() - Task title +- \`description\`: v.optional(v.string()) - Task details +- \`status\`: v.union(v.literal("todo"), v.literal("in_progress"), v.literal("done")) - Current status +- \`projectId\`: v.id("projects") - Parent project +- \`userId\`: v.id("users") - Task creator +- \`assignedTo\`: v.optional(v.id("users")) - Assigned user +- \`dueDate\`: v.optional(v.number()) - Unix timestamp +- \`priority\`: v.optional(v.union(v.literal("low"), v.literal("medium"), v.literal("high"))) + +Indexes: +- \`by_projectId\`: ["projectId"] - list tasks in a project +- \`by_projectId_and_status\`: ["projectId", "status"] - filter by status +- \`by_userId\`: ["userId"] - list user's tasks +- \`by_assignedTo\`: ["assignedTo"] - list assigned tasks + +## Relationships +- users → projects: One-to-many (a user owns many projects) +- users → tasks: One-to-many (a user creates many tasks) +- projects → tasks: One-to-many (a project contains many tasks) +- users → tasks (assigned): Many-to-many (users can be assigned to tasks) + +## Security Notes +- All user data is private to that user +- Projects are private to their owner +- Tasks inherit privacy from their project + +## Generated Files +- convex/schema.ts - Schema definition +- convex/projects/queries.ts - getProject, listUserProjects +- convex/projects/mutations.ts - createProject, updateProject, archiveProject +- convex/tasks/queries.ts - getTask, listProjectTasks, listUserTasks +- convex/tasks/mutations.ts - createTask, updateTask, deleteTask, assignTask + + +## Rules +1. Design for the user's EXACT requirements - no generic schemas +2. Include ALL tables needed for the full feature set +3. Add proper indexes for every query pattern +4. Use optional() for truly nullable fields only +5. Be CONCRETE - specific field names, types, and purposes +6. Output ONLY the schema_proposal block, no additional commentary +`;