Compare commits
11 Commits
main
...
feature/ap
Author | SHA1 | Date | |
---|---|---|---|
|
f6fee20955 | ||
|
74b0c6b9e4 | ||
|
d840aad3e9 | ||
|
89804c5906 | ||
|
aa33ad3031 | ||
|
f79115974c | ||
|
1bb518b1af | ||
|
721ea5cad2 | ||
|
ebfde291ad | ||
|
07ca7dee46 | ||
|
5af35f6920 |
@ -6,18 +6,18 @@ import { z } from "zod"
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import * as schema from "./schema"
|
||||
import {
|
||||
Sandbox,
|
||||
sandbox,
|
||||
sandboxLikes,
|
||||
user,
|
||||
usersToSandboxes,
|
||||
Sandbox,
|
||||
sandbox,
|
||||
sandboxLikes,
|
||||
user,
|
||||
usersToSandboxes,
|
||||
} from "./schema"
|
||||
|
||||
export interface Env {
|
||||
DB: D1Database
|
||||
STORAGE: any
|
||||
KEY: string
|
||||
STORAGE_WORKER_URL: string
|
||||
DB: D1Database
|
||||
STORAGE: any
|
||||
KEY: string
|
||||
STORAGE_WORKER_URL: string
|
||||
}
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
|
||||
@ -25,469 +25,488 @@ export interface Env {
|
||||
// npm run generate
|
||||
// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE>
|
||||
interface SandboxWithLiked extends Sandbox {
|
||||
liked: boolean
|
||||
liked: boolean
|
||||
}
|
||||
|
||||
interface UserResponse extends Omit<schema.User, "sandbox"> {
|
||||
sandbox: SandboxWithLiked[]
|
||||
sandbox: SandboxWithLiked[]
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const success = new Response("Success", { status: 200 })
|
||||
const invalidRequest = new Response("Invalid Request", { status: 400 })
|
||||
const notFound = new Response("Not Found", { status: 404 })
|
||||
const methodNotAllowed = new Response("Method Not Allowed", { status: 405 })
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext
|
||||
): Promise<Response> {
|
||||
const success = new Response("Success", { status: 200 })
|
||||
const invalidRequest = new Response("Invalid Request", { status: 400 })
|
||||
const notFound = new Response("Not Found", { status: 404 })
|
||||
const methodNotAllowed = new Response("Method Not Allowed", { status: 405 })
|
||||
|
||||
const url = new URL(request.url)
|
||||
const path = url.pathname
|
||||
const method = request.method
|
||||
const url = new URL(request.url)
|
||||
const path = url.pathname
|
||||
const method = request.method
|
||||
|
||||
if (request.headers.get("Authorization") !== env.KEY) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
if (request.headers.get("Authorization") !== env.KEY) {
|
||||
return new Response("Unauthorized", { status: 401 })
|
||||
}
|
||||
|
||||
const db = drizzle(env.DB, { schema })
|
||||
const db = drizzle(env.DB, { schema })
|
||||
|
||||
if (path === "/api/sandbox") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
const res = await db.query.sandbox.findFirst({
|
||||
where: (sandbox, { eq }) => eq(sandbox.id, id),
|
||||
with: {
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
return json(res ?? {})
|
||||
} else {
|
||||
const res = await db.select().from(sandbox).all()
|
||||
return json(res ?? {})
|
||||
}
|
||||
} else if (method === "DELETE") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
await db
|
||||
.delete(usersToSandboxes)
|
||||
.where(eq(usersToSandboxes.sandboxId, id))
|
||||
await db.delete(sandbox).where(eq(sandbox.id, id))
|
||||
if (path === "/api/sandbox") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
const res = await db.query.sandbox.findFirst({
|
||||
where: (sandbox, { eq }) => eq(sandbox.id, id),
|
||||
with: {
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
return json(res ?? {})
|
||||
} else {
|
||||
const res = await db.select().from(sandbox).all()
|
||||
return json(res ?? {})
|
||||
}
|
||||
} else if (method === "DELETE") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
await db
|
||||
.delete(usersToSandboxes)
|
||||
.where(eq(usersToSandboxes.sandboxId, id))
|
||||
await db.delete(sandbox).where(eq(sandbox.id, id))
|
||||
|
||||
const deleteStorageRequest = new Request(
|
||||
`${env.STORAGE_WORKER_URL}/api/project`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ sandboxId: id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${env.KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
await env.STORAGE.fetch(deleteStorageRequest)
|
||||
const deleteStorageRequest = new Request(
|
||||
`${env.STORAGE_WORKER_URL}/api/project`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ sandboxId: id }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${env.KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
await env.STORAGE.fetch(deleteStorageRequest)
|
||||
|
||||
return success
|
||||
} else {
|
||||
return invalidRequest
|
||||
}
|
||||
} else if (method === "POST") {
|
||||
const postSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
visibility: z.enum(["public", "private"]).optional(),
|
||||
})
|
||||
return success
|
||||
} else {
|
||||
return invalidRequest
|
||||
}
|
||||
} else if (method === "POST") {
|
||||
const postSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
visibility: z.enum(["public", "private"]).optional(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { id, name, visibility } = postSchema.parse(body)
|
||||
const sb = await db
|
||||
.update(sandbox)
|
||||
.set({ name, visibility })
|
||||
.where(eq(sandbox.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
const body = await request.json()
|
||||
const { id, name, visibility } = postSchema.parse(body)
|
||||
const sb = await db
|
||||
.update(sandbox)
|
||||
.set({ name, visibility })
|
||||
.where(eq(sandbox.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
return success
|
||||
} else if (method === "PUT") {
|
||||
const initSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
userId: z.string(),
|
||||
visibility: z.enum(["public", "private"]),
|
||||
})
|
||||
return success
|
||||
} else if (method === "PUT") {
|
||||
const initSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
userId: z.string(),
|
||||
visibility: z.enum(["public", "private"]),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { type, name, userId, visibility } = initSchema.parse(body)
|
||||
const body = await request.json()
|
||||
const { type, name, userId, visibility } = initSchema.parse(body)
|
||||
|
||||
const userSandboxes = await db
|
||||
.select()
|
||||
.from(sandbox)
|
||||
.where(eq(sandbox.userId, userId))
|
||||
.all()
|
||||
const userSandboxes = await db
|
||||
.select()
|
||||
.from(sandbox)
|
||||
.where(eq(sandbox.userId, userId))
|
||||
.all()
|
||||
|
||||
if (userSandboxes.length >= 8) {
|
||||
return new Response("You reached the maximum # of sandboxes.", {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
if (userSandboxes.length >= 8) {
|
||||
return new Response("You reached the maximum # of sandboxes.", {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
const sb = await db
|
||||
.insert(sandbox)
|
||||
.values({ type, name, userId, visibility, createdAt: new Date() })
|
||||
.returning()
|
||||
.get()
|
||||
const sb = await db
|
||||
.insert(sandbox)
|
||||
.values({ type, name, userId, visibility, createdAt: new Date() })
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
const initStorageRequest = new Request(
|
||||
`${env.STORAGE_WORKER_URL}/api/init`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sandboxId: sb.id, type }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${env.KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
const initStorageRequest = new Request(
|
||||
`${env.STORAGE_WORKER_URL}/api/init`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sandboxId: sb.id, type }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${env.KEY}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await env.STORAGE.fetch(initStorageRequest)
|
||||
await env.STORAGE.fetch(initStorageRequest)
|
||||
|
||||
return new Response(sb.id, { status: 200 })
|
||||
} else {
|
||||
return methodNotAllowed
|
||||
}
|
||||
} else if (path === "/api/sandbox/share") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
const res = await db.query.usersToSandboxes.findMany({
|
||||
where: (uts, { eq }) => eq(uts.userId, id),
|
||||
})
|
||||
return new Response(sb.id, { status: 200 })
|
||||
} else {
|
||||
return methodNotAllowed
|
||||
}
|
||||
} else if (path === "/api/sandbox/share") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
const res = await db.query.usersToSandboxes.findMany({
|
||||
where: (uts, { eq }) => eq(uts.userId, id),
|
||||
})
|
||||
|
||||
const owners = await Promise.all(
|
||||
res.map(async (r) => {
|
||||
const sb = await db.query.sandbox.findFirst({
|
||||
where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId),
|
||||
with: {
|
||||
author: true,
|
||||
},
|
||||
})
|
||||
if (!sb) return
|
||||
return {
|
||||
id: sb.id,
|
||||
name: sb.name,
|
||||
type: sb.type,
|
||||
author: sb.author.name,
|
||||
authorAvatarUrl: sb.author.avatarUrl,
|
||||
sharedOn: r.sharedOn,
|
||||
}
|
||||
})
|
||||
)
|
||||
const owners = await Promise.all(
|
||||
res.map(async (r) => {
|
||||
const sb = await db.query.sandbox.findFirst({
|
||||
where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId),
|
||||
with: {
|
||||
author: true,
|
||||
},
|
||||
})
|
||||
if (!sb) return
|
||||
return {
|
||||
id: sb.id,
|
||||
name: sb.name,
|
||||
type: sb.type,
|
||||
author: sb.author.name,
|
||||
authorAvatarUrl: sb.author.avatarUrl,
|
||||
sharedOn: r.sharedOn,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return json(owners ?? {})
|
||||
} else return invalidRequest
|
||||
} else if (method === "POST") {
|
||||
const shareSchema = z.object({
|
||||
sandboxId: z.string(),
|
||||
email: z.string(),
|
||||
})
|
||||
return json(owners ?? {})
|
||||
} else return invalidRequest
|
||||
} else if (method === "POST") {
|
||||
const shareSchema = z.object({
|
||||
sandboxId: z.string(),
|
||||
email: z.string(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { sandboxId, email } = shareSchema.parse(body)
|
||||
const body = await request.json()
|
||||
const { sandboxId, email } = shareSchema.parse(body)
|
||||
|
||||
const user = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.email, email),
|
||||
with: {
|
||||
sandbox: true,
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
const user = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.email, email),
|
||||
with: {
|
||||
sandbox: true,
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return new Response("No user associated with email.", { status: 400 })
|
||||
}
|
||||
if (!user) {
|
||||
return new Response("No user associated with email.", { status: 400 })
|
||||
}
|
||||
|
||||
if (user.sandbox.find((sb) => sb.id === sandboxId)) {
|
||||
return new Response("Cannot share with yourself!", { status: 400 })
|
||||
}
|
||||
if (user.sandbox.find((sb) => sb.id === sandboxId)) {
|
||||
return new Response("Cannot share with yourself!", { status: 400 })
|
||||
}
|
||||
|
||||
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
|
||||
return new Response("User already has access.", { status: 400 })
|
||||
}
|
||||
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
|
||||
return new Response("User already has access.", { status: 400 })
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(usersToSandboxes)
|
||||
.values({ userId: user.id, sandboxId, sharedOn: new Date() })
|
||||
.get()
|
||||
await db
|
||||
.insert(usersToSandboxes)
|
||||
.values({ userId: user.id, sandboxId, sharedOn: new Date() })
|
||||
.get()
|
||||
|
||||
return success
|
||||
} else if (method === "DELETE") {
|
||||
const deleteShareSchema = z.object({
|
||||
sandboxId: z.string(),
|
||||
userId: z.string(),
|
||||
})
|
||||
return success
|
||||
} else if (method === "DELETE") {
|
||||
const deleteShareSchema = z.object({
|
||||
sandboxId: z.string(),
|
||||
userId: z.string(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { sandboxId, userId } = deleteShareSchema.parse(body)
|
||||
const body = await request.json()
|
||||
const { sandboxId, userId } = deleteShareSchema.parse(body)
|
||||
|
||||
await db
|
||||
.delete(usersToSandboxes)
|
||||
.where(
|
||||
and(
|
||||
eq(usersToSandboxes.userId, userId),
|
||||
eq(usersToSandboxes.sandboxId, sandboxId)
|
||||
)
|
||||
)
|
||||
await db
|
||||
.delete(usersToSandboxes)
|
||||
.where(
|
||||
and(
|
||||
eq(usersToSandboxes.userId, userId),
|
||||
eq(usersToSandboxes.sandboxId, sandboxId)
|
||||
)
|
||||
)
|
||||
|
||||
return success
|
||||
} else return methodNotAllowed
|
||||
} else if (path === "/api/user") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
return success
|
||||
} else return methodNotAllowed
|
||||
} else if (path === "/api/user") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, id),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === id),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else if (params.has("username")) {
|
||||
const username = params.get("username") as string
|
||||
const userId = params.get("currentUserId")
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === userId),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else {
|
||||
const res = await db.select().from(user).all()
|
||||
return json(res ?? {})
|
||||
}
|
||||
} else if (method === "POST") {
|
||||
const userSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
username: z.string(),
|
||||
avatarUrl: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
generations: z.number().optional(),
|
||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
|
||||
tierExpiresAt: z.number().optional(),
|
||||
lastResetDate: z.number().optional(),
|
||||
})
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, id),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === id),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else if (params.has("username")) {
|
||||
const username = params.get("username") as string
|
||||
const userId = params.get("currentUserId")
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === userId),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else {
|
||||
const res = await db.select().from(user).all()
|
||||
return json(res ?? {})
|
||||
}
|
||||
} else if (method === "POST") {
|
||||
const userSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
username: z.string(),
|
||||
avatarUrl: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
generations: z.number().optional(),
|
||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
|
||||
tierExpiresAt: z.number().optional(),
|
||||
lastResetDate: z.number().optional(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const body = await request.json()
|
||||
|
||||
const { id, name, email, username, avatarUrl, createdAt, generations, tier, tierExpiresAt, lastResetDate } = userSchema.parse(body)
|
||||
const res = await db
|
||||
.insert(user)
|
||||
.values({
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
username,
|
||||
avatarUrl,
|
||||
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
||||
generations,
|
||||
tier,
|
||||
tierExpiresAt,
|
||||
lastResetDate,
|
||||
})
|
||||
.returning()
|
||||
.get()
|
||||
return json({ res })
|
||||
} else if (method === "DELETE") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
await db.delete(user).where(eq(user.id, id))
|
||||
return success
|
||||
} else return invalidRequest
|
||||
} else if (method === "PUT") {
|
||||
const updateUserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
username: z.string().optional(),
|
||||
avatarUrl: z.string().optional(),
|
||||
generations: z.number().optional(),
|
||||
})
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
username,
|
||||
avatarUrl,
|
||||
createdAt,
|
||||
generations,
|
||||
tier,
|
||||
tierExpiresAt,
|
||||
lastResetDate,
|
||||
} = userSchema.parse(body)
|
||||
const res = await db
|
||||
.insert(user)
|
||||
.values({
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
username,
|
||||
avatarUrl,
|
||||
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
||||
generations,
|
||||
tier,
|
||||
tierExpiresAt,
|
||||
lastResetDate,
|
||||
})
|
||||
.returning()
|
||||
.get()
|
||||
return json({ res })
|
||||
} else if (method === "DELETE") {
|
||||
const params = url.searchParams
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
await db.delete(user).where(eq(user.id, id))
|
||||
return success
|
||||
} else return invalidRequest
|
||||
} else if (method === "PUT") {
|
||||
const updateUserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
username: z.string().optional(),
|
||||
avatarUrl: z.string().optional(),
|
||||
generations: z.number().optional(),
|
||||
})
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validatedData = updateUserSchema.parse(body)
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validatedData = updateUserSchema.parse(body)
|
||||
|
||||
const { id, username, ...updateData } = validatedData
|
||||
const { id, username, ...updateData } = validatedData
|
||||
|
||||
// If username is being updated, check for existing username
|
||||
if (username) {
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.username, username))
|
||||
.get()
|
||||
if (existingUser && existingUser.id !== id) {
|
||||
return json({ error: "Username already exists" }, { status: 409 })
|
||||
}
|
||||
}
|
||||
// If username is being updated, check for existing username
|
||||
if (username) {
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.username, username))
|
||||
.get()
|
||||
if (existingUser && existingUser.id !== id) {
|
||||
return json({ error: "Username already exists" }, { status: 409 })
|
||||
}
|
||||
}
|
||||
|
||||
const cleanUpdateData = {
|
||||
...updateData,
|
||||
...(username ? { username } : {}),
|
||||
}
|
||||
const cleanUpdateData = {
|
||||
...updateData,
|
||||
...(username ? { username } : {}),
|
||||
}
|
||||
|
||||
const res = await db
|
||||
.update(user)
|
||||
.set(cleanUpdateData)
|
||||
.where(eq(user.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
const res = await db
|
||||
.update(user)
|
||||
.set(cleanUpdateData)
|
||||
.where(eq(user.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
if (!res) {
|
||||
return json({ error: "User not found" }, { status: 404 })
|
||||
}
|
||||
if (!res) {
|
||||
return json({ error: "User not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return json({ res })
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return json({ error: error.errors }, { status: 400 })
|
||||
}
|
||||
return json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
} else {
|
||||
return methodNotAllowed
|
||||
}
|
||||
} else if (path === "/api/user/check-username") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
const username = params.get("username")
|
||||
return json({ res })
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return json({ error: error.errors }, { status: 400 })
|
||||
}
|
||||
return json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
} else {
|
||||
return methodNotAllowed
|
||||
}
|
||||
} else if (path === "/api/user/check-username") {
|
||||
if (method === "GET") {
|
||||
const params = url.searchParams
|
||||
const username = params.get("username")
|
||||
|
||||
if (!username) return invalidRequest
|
||||
if (!username) return invalidRequest
|
||||
|
||||
const exists = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
})
|
||||
const exists = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
})
|
||||
|
||||
return json({ exists: !!exists })
|
||||
}
|
||||
return methodNotAllowed
|
||||
} else if (path === "/api/user/increment-generations" && method === "POST") {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
return json({ exists: !!exists })
|
||||
}
|
||||
return methodNotAllowed
|
||||
} else if (
|
||||
path === "/api/user/increment-generations" &&
|
||||
method === "POST"
|
||||
) {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { userId } = schema.parse(body)
|
||||
const body = await request.json()
|
||||
const { userId } = schema.parse(body)
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({ generations: sql`${user.generations} + 1` })
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
await db
|
||||
.update(user)
|
||||
.set({ generations: sql`${user.generations} + 1` })
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
|
||||
return success
|
||||
} else if (path === "/api/user/update-tier" && method === "POST") {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
|
||||
tierExpiresAt: z.date(),
|
||||
})
|
||||
return success
|
||||
} else if (path === "/api/user/update-tier" && method === "POST") {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
|
||||
tierExpiresAt: z.date(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { userId, tier, tierExpiresAt } = schema.parse(body)
|
||||
const body = await request.json()
|
||||
const { userId, tier, tierExpiresAt } = schema.parse(body)
|
||||
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
tier,
|
||||
tierExpiresAt: tierExpiresAt.getTime(),
|
||||
// Reset generations when upgrading tier
|
||||
generations: 0
|
||||
})
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
tier,
|
||||
tierExpiresAt: tierExpiresAt.getTime(),
|
||||
// Reset generations when upgrading tier
|
||||
generations: 0,
|
||||
})
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
|
||||
return success
|
||||
} else if (path === "/api/user/check-reset" && method === "POST") {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
return success
|
||||
} else if (path === "/api/user/check-reset" && method === "POST") {
|
||||
const schema = z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { userId } = schema.parse(body)
|
||||
const body = await request.json()
|
||||
const { userId } = schema.parse(body)
|
||||
|
||||
const dbUser = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, userId),
|
||||
})
|
||||
const dbUser = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, userId),
|
||||
})
|
||||
|
||||
if (!dbUser) {
|
||||
return new Response("User not found", { status: 404 })
|
||||
}
|
||||
if (!dbUser) {
|
||||
return new Response("User not found", { status: 404 })
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const lastReset = dbUser.lastResetDate ? new Date(dbUser.lastResetDate) : new Date(0)
|
||||
const now = new Date()
|
||||
const lastReset = dbUser.lastResetDate
|
||||
? new Date(dbUser.lastResetDate)
|
||||
: new Date(0)
|
||||
|
||||
if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) {
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
generations: 0,
|
||||
lastResetDate: now.getTime()
|
||||
})
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
|
||||
return new Response("Reset successful", { status: 200 })
|
||||
}
|
||||
if (
|
||||
now.getMonth() !== lastReset.getMonth() ||
|
||||
now.getFullYear() !== lastReset.getFullYear()
|
||||
) {
|
||||
await db
|
||||
.update(user)
|
||||
.set({
|
||||
generations: 0,
|
||||
lastResetDate: now.getTime(),
|
||||
})
|
||||
.where(eq(user.id, userId))
|
||||
.get()
|
||||
|
||||
return new Response("No reset needed", { status: 200 })
|
||||
} else return notFound
|
||||
},
|
||||
return new Response("Reset successful", { status: 200 })
|
||||
}
|
||||
|
||||
return new Response("No reset needed", { status: 200 })
|
||||
} else return notFound
|
||||
},
|
||||
}
|
||||
|
@ -4,112 +4,112 @@ import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
|
||||
// #region Tables
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
avatarUrl: text("avatarUrl"),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
generations: integer("generations").default(0),
|
||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||
tierExpiresAt: integer("tierExpiresAt"),
|
||||
lastResetDate: integer("lastResetDate"),
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
avatarUrl: text("avatarUrl"),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
generations: integer("generations").default(0),
|
||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||
tierExpiresAt: integer("tierExpiresAt"),
|
||||
lastResetDate: integer("lastResetDate"),
|
||||
})
|
||||
|
||||
export type User = typeof user.$inferSelect
|
||||
|
||||
export const sandbox = sqliteTable("sandbox", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
likeCount: integer("likeCount").default(0),
|
||||
viewCount: integer("viewCount").default(0),
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
.primaryKey()
|
||||
.unique(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
likeCount: integer("likeCount").default(0),
|
||||
viewCount: integer("viewCount").default(0),
|
||||
})
|
||||
|
||||
export type Sandbox = typeof sandbox.$inferSelect
|
||||
|
||||
export const sandboxLikes = sqliteTable(
|
||||
"sandbox_likes",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandbox_id")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
||||
})
|
||||
"sandbox_likes",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandbox_id")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
||||
})
|
||||
)
|
||||
|
||||
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandboxId")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandboxId")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||
})
|
||||
|
||||
// #region Relations
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxLikesRelations = relations(sandboxLikes, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [sandboxLikes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
sandbox: one(sandbox, {
|
||||
fields: [sandboxLikes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [sandboxLikes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
sandbox: one(sandbox, {
|
||||
fields: [sandboxLikes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const usersToSandboxesRelations = relations(
|
||||
usersToSandboxes,
|
||||
({ one }) => ({
|
||||
group: one(sandbox, {
|
||||
fields: [usersToSandboxes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [usersToSandboxes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
})
|
||||
usersToSandboxes,
|
||||
({ one }) => ({
|
||||
group: one(sandbox, {
|
||||
fields: [usersToSandboxes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [usersToSandboxes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// #endregion
|
||||
|
@ -7,7 +7,6 @@ PORT=4000
|
||||
WORKERS_KEY=
|
||||
DATABASE_WORKER_URL=
|
||||
STORAGE_WORKER_URL=
|
||||
AI_WORKER_URL=
|
||||
E2B_API_KEY=
|
||||
DOKKU_HOST=
|
||||
DOKKU_USERNAME=
|
||||
|
@ -3,18 +3,21 @@ CLERK_SECRET_KEY=
|
||||
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
|
||||
LIVEBLOCKS_SECRET_KEY=
|
||||
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_SERVER_PORT=4000
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
||||
|
||||
# Set WORKER_URLs after deploying the workers.
|
||||
# Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
|
||||
# These URLs should begin with https:// in production
|
||||
NEXT_PUBLIC_DATABASE_WORKER_URL=
|
||||
NEXT_PUBLIC_STORAGE_WORKER_URL=
|
||||
NEXT_PUBLIC_AI_WORKER_URL=
|
||||
NEXT_PUBLIC_WORKERS_KEY=
|
||||
NEXT_PUBLIC_DATABASE_WORKER_URL=https://database.your-worker.workers.dev
|
||||
NEXT_PUBLIC_STORAGE_WORKER_URL=https://storage.your-worker.workers.dev
|
||||
NEXT_PUBLIC_WORKERS_KEY=SUPERDUPERSECRET
|
||||
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
|
||||
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
|
||||
|
||||
ANTHROPIC_API_KEY=
|
||||
OPENAI_API_KEY=
|
@ -1,12 +1,59 @@
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import {
|
||||
ignoredFiles,
|
||||
ignoredFolders,
|
||||
} from "@/components/editor/AIChat/lib/ignored-paths"
|
||||
import { templateConfigs } from "@/lib/templates"
|
||||
import { TIERS } from "@/lib/tiers"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||
})
|
||||
|
||||
// Format file structure for context
|
||||
function formatFileStructure(
|
||||
items: (TFile | TFolder)[] | undefined,
|
||||
prefix = ""
|
||||
): string {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return "No files available"
|
||||
}
|
||||
|
||||
// Sort items to show folders first, then files
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
if (a.type === b.type) return a.name.localeCompare(b.name)
|
||||
return a.type === "folder" ? -1 : 1
|
||||
})
|
||||
|
||||
return sortedItems
|
||||
.map((item) => {
|
||||
if (
|
||||
item.type === "file" &&
|
||||
!ignoredFiles.some(
|
||||
(pattern) =>
|
||||
item.name.endsWith(pattern.replace("*", "")) ||
|
||||
item.name === pattern
|
||||
)
|
||||
) {
|
||||
return `${prefix}├── ${item.name}`
|
||||
} else if (
|
||||
item.type === "folder" &&
|
||||
!ignoredFolders.some((folder) => folder === item.name)
|
||||
) {
|
||||
const folderContent = formatFileStructure(
|
||||
item.children,
|
||||
`${prefix}│ `
|
||||
)
|
||||
return `${prefix}├── ${item.name}/\n${folderContent}`
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await currentUser()
|
||||
@ -43,42 +90,48 @@ export async function POST(request: Request) {
|
||||
const userData = await dbUser.json()
|
||||
|
||||
// Get tier settings
|
||||
const tierSettings = TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||
const tierSettings =
|
||||
TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||
if (userData.generations >= tierSettings.generations) {
|
||||
return new Response(
|
||||
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
||||
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
messages,
|
||||
context,
|
||||
activeFileContent,
|
||||
const {
|
||||
messages,
|
||||
context,
|
||||
activeFileContent,
|
||||
isEditMode,
|
||||
fileName,
|
||||
line,
|
||||
templateType
|
||||
templateType,
|
||||
files,
|
||||
projectName,
|
||||
} = await request.json()
|
||||
|
||||
// Get template configuration
|
||||
const templateConfig = templateConfigs[templateType]
|
||||
|
||||
// Create template context
|
||||
const templateContext = templateConfig ? `
|
||||
const templateContext = templateConfig
|
||||
? `
|
||||
Project Template: ${templateConfig.name}
|
||||
|
||||
File Structure:
|
||||
${Object.entries(templateConfig.fileStructure)
|
||||
.map(([path, info]) => `${path} - ${info.description}`)
|
||||
.join('\n')}
|
||||
Current File Structure:
|
||||
${files ? formatFileStructure(files) : "No files available"}
|
||||
|
||||
Conventions:
|
||||
${templateConfig.conventions.join('\n')}
|
||||
${templateConfig.conventions.join("\n")}
|
||||
|
||||
Dependencies:
|
||||
${JSON.stringify(templateConfig.dependencies, null, 2)}
|
||||
` : ''
|
||||
|
||||
Scripts:
|
||||
${JSON.stringify(templateConfig.scripts, null, 2)}
|
||||
`
|
||||
: ""
|
||||
|
||||
// Create system message based on mode
|
||||
let systemMessage
|
||||
@ -100,13 +153,23 @@ Instructions: ${messages[0].content}
|
||||
|
||||
Respond only with the modified code that can directly replace the existing code.`
|
||||
} else {
|
||||
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
|
||||
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. When providing code:
|
||||
|
||||
\`\`\`python
|
||||
print("Hello, World!")
|
||||
\`\`\`
|
||||
|
||||
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
1. Format it using triple backticks (\`\`\`) with the appropriate language identifier.
|
||||
2. Always specify the complete file path in the format:
|
||||
${projectName}/filepath/to/file.ext
|
||||
|
||||
3. If creating a new file, specify the path as:
|
||||
${projectName}/filepath/to/file.ext (new file)
|
||||
|
||||
4. Format your code blocks as:
|
||||
|
||||
${projectName}/filepath/to/file.ext
|
||||
\`\`\`language
|
||||
code here
|
||||
\`\`\`
|
||||
|
||||
If multiple files are involved, repeat the format for each file. Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||
|
||||
This is the project template:
|
||||
${templateContext}
|
||||
@ -146,7 +209,10 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
||||
if (
|
||||
chunk.type === "content_block_delta" &&
|
||||
chunk.delta.type === "text_delta"
|
||||
) {
|
||||
controller.enqueue(encoder.encode(chunk.delta.text))
|
||||
}
|
||||
}
|
||||
@ -164,8 +230,8 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
||||
} catch (error) {
|
||||
console.error("AI generation error:", error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Internal Server Error",
|
||||
error instanceof Error ? error.message : "Internal Server Error",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
frontend/app/api/merge/route.ts
Normal file
69
frontend/app/api/merge/route.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import OpenAI from "openai"
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { originalCode, newCode, fileName } = await request.json()
|
||||
|
||||
const systemPrompt = `You are a code merging assistant. Your task is to merge the new code snippet with the original file content while:
|
||||
1. Preserving the original file's functionality
|
||||
2. Ensuring proper integration of the new code
|
||||
3. Maintaining consistent style and formatting
|
||||
4. Resolving any potential conflicts
|
||||
5. Output ONLY the raw code without any:
|
||||
- Code fence markers (\`\`\`)
|
||||
- Language identifiers (typescript, javascript, etc.)
|
||||
- Explanations or comments
|
||||
- Markdown formatting
|
||||
|
||||
The output should be the exact code that will replace the existing code, nothing more and nothing less.
|
||||
|
||||
Important: When merging, preserve the original code structure as much as possible. Only make necessary changes to integrate the new code while maintaining the original code's organization and style.`
|
||||
|
||||
const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: mergedCode },
|
||||
],
|
||||
prediction: {
|
||||
type: "content",
|
||||
content: mergedCode,
|
||||
},
|
||||
stream: true,
|
||||
})
|
||||
|
||||
// Clean and stream response
|
||||
const encoder = new TextEncoder()
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
let buffer = ""
|
||||
for await (const chunk of response) {
|
||||
if (chunk.choices[0]?.delta?.content) {
|
||||
buffer += chunk.choices[0].delta.content
|
||||
// Clean any code fence markers that might appear in the stream
|
||||
const cleanedContent = buffer
|
||||
.replace(/^```[\w-]*\n|```\s*$/gm, "") // Remove code fences
|
||||
.replace(/^(javascript|typescript|python|html|css)\n/gm, "") // Remove language identifiers
|
||||
controller.enqueue(encoder.encode(cleanedContent))
|
||||
buffer = ""
|
||||
}
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Merge error:", error)
|
||||
return new Response(
|
||||
error instanceof Error ? error.message : "Failed to merge code",
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@ -8,9 +8,9 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const { tier } = await request.json()
|
||||
|
||||
|
||||
// handle payment processing here
|
||||
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/update-tier`,
|
||||
{
|
||||
@ -39,4 +39,4 @@ export async function POST(request: Request) {
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -175,3 +175,23 @@
|
||||
.tab-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.added-line-decoration {
|
||||
background-color: rgba(0, 255, 0, 0.1);
|
||||
}
|
||||
|
||||
.removed-line-decoration {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.added-line-glyph {
|
||||
background-color: #28a745;
|
||||
width: 4px !important;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.removed-line-glyph {
|
||||
background-color: #dc3545;
|
||||
width: 4px !important;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
77
frontend/components/editor/AIChat/ApplyButton.tsx
Normal file
77
frontend/components/editor/AIChat/ApplyButton.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { Check, Loader2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import { Button } from "../../ui/button"
|
||||
|
||||
interface ApplyButtonProps {
|
||||
code: string
|
||||
activeFileName: string
|
||||
activeFileContent: string
|
||||
editorRef: { current: any }
|
||||
onApply: (mergedCode: string, originalCode: string) => void
|
||||
}
|
||||
|
||||
export default function ApplyButton({
|
||||
code,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
onApply,
|
||||
}: ApplyButtonProps) {
|
||||
const [isApplying, setIsApplying] = useState(false)
|
||||
|
||||
const handleApply = async () => {
|
||||
setIsApplying(true)
|
||||
try {
|
||||
const response = await fetch("/api/merge", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
originalCode: activeFileContent,
|
||||
newCode: String(code),
|
||||
fileName: activeFileName,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let mergedCode = ""
|
||||
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
mergedCode += decoder.decode(value, { stream: true })
|
||||
}
|
||||
}
|
||||
onApply(mergedCode.trim(), activeFileContent)
|
||||
} catch (error) {
|
||||
console.error("Error applying code:", error)
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to apply code changes"
|
||||
)
|
||||
} finally {
|
||||
setIsApplying(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
disabled={isApplying}
|
||||
>
|
||||
{isApplying ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
@ -13,6 +13,13 @@ export default function ChatMessage({
|
||||
setContext,
|
||||
setIsContextExpanded,
|
||||
socket,
|
||||
handleApplyCode,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
selectFile,
|
||||
}: MessageProps) {
|
||||
// State for expanded message index
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||
@ -104,7 +111,14 @@ export default function ChatMessage({
|
||||
const components = createMarkdownComponents(
|
||||
renderCopyButton,
|
||||
renderMarkdownElement,
|
||||
askAboutCode
|
||||
askAboutCode,
|
||||
activeFileName,
|
||||
activeFileContent,
|
||||
editorRef,
|
||||
handleApplyCode,
|
||||
selectFile,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
)
|
||||
|
||||
return (
|
||||
@ -201,7 +215,8 @@ export default function ChatMessage({
|
||||
|
||||
// Parse context to tabs for context tabs component
|
||||
function parseContextToTabs(context: string) {
|
||||
const sections = context.split(/(?=File |Code from )/)
|
||||
// Use specific regex patterns to avoid matching import statements
|
||||
const sections = context.split(/(?=File |Code from |Image \d{1,2}:)/)
|
||||
return sections
|
||||
.map((section, index) => {
|
||||
const lines = section.trim().split("\n")
|
||||
@ -211,16 +226,29 @@ function parseContextToTabs(context: string) {
|
||||
// Remove code block markers for display
|
||||
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
||||
|
||||
// Determine if the context is a file or code
|
||||
// Determine the type of context
|
||||
const isFile = titleLine.startsWith("File ")
|
||||
const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
|
||||
const isImage = titleLine.startsWith("Image ")
|
||||
const type = isFile ? "file" : isImage ? "image" : "code"
|
||||
const name = titleLine
|
||||
.replace(/^(File |Code from |Image )/, "")
|
||||
.replace(":", "")
|
||||
.trim()
|
||||
|
||||
// Skip if the content is empty or if it's just an import statement
|
||||
if (!content || content.trim().startsWith('from "')) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: `context-${index}`,
|
||||
type: isFile ? ("file" as const) : ("code" as const),
|
||||
type: type as "file" | "code" | "image",
|
||||
name: name,
|
||||
content: content,
|
||||
}
|
||||
})
|
||||
.filter((tab) => tab.content.length > 0)
|
||||
.filter(
|
||||
(tab): tab is NonNullable<typeof tab> =>
|
||||
tab !== null && tab.content.length > 0
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { TFile } from "@/lib/types"
|
||||
import { X, ChevronDown } from "lucide-react"
|
||||
import { ChevronDown, X } from "lucide-react"
|
||||
import { nanoid } from "nanoid"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import LoadingDots from "../../ui/LoadingDots"
|
||||
@ -18,6 +18,11 @@ export default function AIChat({
|
||||
lastCopiedRangeRef,
|
||||
files,
|
||||
templateType,
|
||||
handleApplyCode,
|
||||
selectFile,
|
||||
mergeDecorationsCollection,
|
||||
setMergeDecorationsCollection,
|
||||
projectName,
|
||||
}: AIChatProps) {
|
||||
// Initialize socket and messages
|
||||
const { socket } = useSocket()
|
||||
@ -53,7 +58,7 @@ export default function AIChat({
|
||||
// scroll to bottom of chat when messages change
|
||||
const scrollToBottom = (force: boolean = false) => {
|
||||
if (!chatContainerRef.current || (!autoScroll && !force)) return
|
||||
|
||||
|
||||
chatContainerRef.current.scrollTo({
|
||||
top: chatContainerRef.current.scrollHeight,
|
||||
behavior: force ? "smooth" : "auto",
|
||||
@ -63,10 +68,10 @@ export default function AIChat({
|
||||
// function to handle scroll events
|
||||
const handleScroll = () => {
|
||||
if (!chatContainerRef.current) return
|
||||
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current
|
||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 50
|
||||
|
||||
|
||||
setAutoScroll(isAtBottom)
|
||||
setShowScrollButton(!isAtBottom)
|
||||
}
|
||||
@ -75,8 +80,8 @@ export default function AIChat({
|
||||
useEffect(() => {
|
||||
const container = chatContainerRef.current
|
||||
if (container) {
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
container.addEventListener("scroll", handleScroll)
|
||||
return () => container.removeEventListener("scroll", handleScroll)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -125,6 +130,8 @@ export default function AIChat({
|
||||
} else if (tab.type === "code") {
|
||||
const cleanContent = formatCodeContent(tab.content)
|
||||
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||
} else if (tab.type === "image") {
|
||||
return `Image ${tab.name}:\n${tab.content}`
|
||||
}
|
||||
return `${tab.name}:\n${tab.content}`
|
||||
})
|
||||
@ -146,10 +153,10 @@ export default function AIChat({
|
||||
abortControllerRef,
|
||||
activeFileContent,
|
||||
false,
|
||||
templateType
|
||||
templateType,
|
||||
files,
|
||||
projectName
|
||||
)
|
||||
// Clear context tabs after sending
|
||||
setContextTabs([])
|
||||
}
|
||||
|
||||
// Set context for the chat
|
||||
@ -167,6 +174,22 @@ export default function AIChat({
|
||||
addContextTab("code", name, context, range)
|
||||
}
|
||||
|
||||
// update context tabs when file contents change
|
||||
useEffect(() => {
|
||||
setContextTabs((prevTabs) =>
|
||||
prevTabs.map((tab) => {
|
||||
if (tab.type === "file" && tab.name === activeFileName) {
|
||||
const fileExt = tab.name.split(".").pop() || "txt"
|
||||
return {
|
||||
...tab,
|
||||
content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``,
|
||||
}
|
||||
}
|
||||
return tab
|
||||
})
|
||||
)
|
||||
}, [activeFileContent, activeFileName])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
@ -197,10 +220,17 @@ export default function AIChat({
|
||||
setContext={setContext}
|
||||
setIsContextExpanded={setIsContextExpanded}
|
||||
socket={socket}
|
||||
handleApplyCode={handleApplyCode}
|
||||
activeFileName={activeFileName}
|
||||
activeFileContent={activeFileContent}
|
||||
editorRef={editorRef}
|
||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
||||
selectFile={selectFile}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <LoadingDots />}
|
||||
|
||||
|
||||
{/* Add scroll to bottom button */}
|
||||
{showScrollButton && (
|
||||
<button
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import React from "react"
|
||||
|
||||
// Stringify content for chat message component
|
||||
@ -91,7 +92,9 @@ export const handleSend = async (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string,
|
||||
isEditMode: boolean = false,
|
||||
templateType: string
|
||||
templateType: string,
|
||||
files: (TFile | TFolder)[],
|
||||
projectName: string
|
||||
) => {
|
||||
// Return if input is empty and context is null
|
||||
if (input.trim() === "" && !context) return
|
||||
@ -131,22 +134,22 @@ export const handleSend = async (
|
||||
}))
|
||||
|
||||
// Fetch AI response for chat message component
|
||||
const response = await fetch("/api/ai",
|
||||
{
|
||||
const response = await fetch("/api/ai", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
isEditMode: isEditMode,
|
||||
templateType: templateType,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
}
|
||||
)
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: anthropicMessages,
|
||||
context: context || undefined,
|
||||
activeFileContent: activeFileContent,
|
||||
isEditMode: isEditMode,
|
||||
templateType: templateType,
|
||||
files: files,
|
||||
projectName: projectName,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
|
||||
// Throw error if response is not ok
|
||||
if (!response.ok) {
|
||||
@ -201,7 +204,8 @@ export const handleSend = async (
|
||||
console.error("Error fetching AI response:", error)
|
||||
const errorMessage = {
|
||||
role: "assistant" as const,
|
||||
content: error.message || "Sorry, I encountered an error. Please try again.",
|
||||
content:
|
||||
error.message || "Sorry, I encountered an error. Please try again.",
|
||||
}
|
||||
setMessages((prev) => [...prev, errorMessage])
|
||||
}
|
||||
@ -239,3 +243,11 @@ export const looksLikeCode = (text: string): boolean => {
|
||||
|
||||
return codeIndicators.some((pattern) => pattern.test(text))
|
||||
}
|
||||
|
||||
// Add this new function after looksLikeCode function
|
||||
export const isFilePath = (text: string): boolean => {
|
||||
// Match patterns like next/styles/SignIn.module.css or path/to/file.ext (new file)
|
||||
const pattern =
|
||||
/^(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/
|
||||
return pattern.test(text)
|
||||
}
|
||||
|
@ -1,15 +1,26 @@
|
||||
import { CornerUpLeft } from "lucide-react"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
import { TTab } from "@/lib/types"
|
||||
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
|
||||
import monaco from "monaco-editor"
|
||||
import { Components } from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { Button } from "../../../ui/button"
|
||||
import { stringifyContent } from "./chatUtils"
|
||||
import ApplyButton from "../ApplyButton"
|
||||
import { isFilePath, stringifyContent } from "./chatUtils"
|
||||
|
||||
// Create markdown components for chat message component
|
||||
export const createMarkdownComponents = (
|
||||
renderCopyButton: (text: any) => JSX.Element,
|
||||
renderMarkdownElement: (props: any) => JSX.Element,
|
||||
askAboutCode: (code: any) => void
|
||||
askAboutCode: (code: any) => void,
|
||||
activeFileName: string,
|
||||
activeFileContent: string,
|
||||
editorRef: any,
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void,
|
||||
selectFile: (tab: TTab) => void,
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
): Components => ({
|
||||
code: ({
|
||||
node,
|
||||
@ -25,39 +36,92 @@ export const createMarkdownComponents = (
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
<div className="relative border border-input rounded-md mt-8 my-2 translate-y-[-1rem]">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 rounded-tl">
|
||||
{match[1]}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex">
|
||||
{renderCopyButton(children)}
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
askAboutCode(children)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
<div className="sticky top-0 right-0 flex justify-end z-10">
|
||||
<div className="flex border border-input shadow-lg bg-background rounded-md">
|
||||
{renderCopyButton(children)}
|
||||
<div className="w-px bg-input"></div>
|
||||
{!mergeDecorationsCollection ? (
|
||||
<ApplyButton
|
||||
code={String(children)}
|
||||
activeFileName={activeFileName}
|
||||
activeFileContent={activeFileContent}
|
||||
editorRef={editorRef}
|
||||
onApply={handleApplyCode}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (
|
||||
setMergeDecorationsCollection &&
|
||||
mergeDecorationsCollection &&
|
||||
editorRef?.current
|
||||
) {
|
||||
mergeDecorationsCollection?.clear()
|
||||
setMergeDecorationsCollection(undefined)
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
title="Accept Changes"
|
||||
>
|
||||
<Check className="w-4 h-4 text-green-500" />
|
||||
</Button>
|
||||
<div className="w-px bg-input"></div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (editorRef?.current && mergeDecorationsCollection) {
|
||||
const model = editorRef.current.getModel()
|
||||
if (model && (model as any).originalContent) {
|
||||
editorRef.current?.setValue(
|
||||
(model as any).originalContent
|
||||
)
|
||||
mergeDecorationsCollection.clear()
|
||||
setMergeDecorationsCollection?.(undefined)
|
||||
}
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
title="Discard Changes"
|
||||
>
|
||||
<X className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div className="w-px bg-input"></div>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
askAboutCode(children)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
@ -66,8 +130,62 @@ export const createMarkdownComponents = (
|
||||
)
|
||||
},
|
||||
// Render markdown elements
|
||||
p: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
p: ({ node, children, ...props }) => {
|
||||
const content = stringifyContent(children)
|
||||
const { socket } = useSocket()
|
||||
|
||||
if (isFilePath(content)) {
|
||||
const isNewFile = content.endsWith("(new file)")
|
||||
const filePath = (
|
||||
isNewFile ? content.replace(" (new file)", "") : content
|
||||
)
|
||||
.split("/")
|
||||
.filter((part, index) => index !== 0)
|
||||
.join("/")
|
||||
|
||||
const handleFileClick = () => {
|
||||
if (isNewFile) {
|
||||
socket?.emit(
|
||||
"createFile",
|
||||
{
|
||||
name: filePath,
|
||||
},
|
||||
(response: any) => {
|
||||
if (response.success) {
|
||||
const tab: TTab = {
|
||||
id: filePath,
|
||||
name: filePath.split("/").pop() || "",
|
||||
saved: true,
|
||||
type: "file",
|
||||
}
|
||||
selectFile(tab)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
const tab: TTab = {
|
||||
id: filePath,
|
||||
name: filePath.split("/").pop() || "",
|
||||
saved: true,
|
||||
type: "file",
|
||||
}
|
||||
selectFile(tab)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleFileClick}
|
||||
className="group flex items-center gap-2 px-2 py-1 bg-secondary/50 rounded-md my-2 text-xs hover:bg-secondary cursor-pointer w-fit"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
<span className="font-mono group-hover:underline">{content}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return renderMarkdownElement({ node, children, ...props })
|
||||
},
|
||||
h1: ({ node, children, ...props }) =>
|
||||
renderMarkdownElement({ node, children, ...props }),
|
||||
h2: ({ node, children, ...props }) =>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TemplateConfig } from "@/lib/templates"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { TFile, TFolder, TTab } from "@/lib/types"
|
||||
import * as monaco from "monaco-editor"
|
||||
import { Socket } from "socket.io-client"
|
||||
|
||||
@ -56,6 +56,11 @@ export interface AIChatProps {
|
||||
files: (TFile | TFolder)[]
|
||||
templateType: string
|
||||
templateConfig?: TemplateConfig
|
||||
projectName: string
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
selectFile: (tab: TTab) => void
|
||||
}
|
||||
|
||||
// Chat input props interface
|
||||
@ -105,6 +110,13 @@ export interface MessageProps {
|
||||
) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
socket: Socket | null
|
||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
||||
activeFileName: string
|
||||
activeFileContent: string
|
||||
editorRef: any
|
||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
||||
selectFile: (tab: TTab) => void
|
||||
}
|
||||
|
||||
// Context tabs props interface
|
||||
|
@ -66,15 +66,17 @@ export default function GenerateInput({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: regenerate ? currentPrompt : input
|
||||
}],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: regenerate ? currentPrompt : input,
|
||||
},
|
||||
],
|
||||
context: null,
|
||||
activeFileContent: data.code,
|
||||
isEditMode: true,
|
||||
fileName: data.fileName,
|
||||
line: data.line
|
||||
line: data.line,
|
||||
}),
|
||||
})
|
||||
|
||||
|
@ -104,6 +104,13 @@ export default function CodeEditor({
|
||||
// Added this state to track the most recent content for each file
|
||||
const [fileContents, setFileContents] = useState<Record<string, string>>({})
|
||||
|
||||
// Apply Button merger decoration state
|
||||
const [mergeDecorations, setMergeDecorations] = useState<
|
||||
monaco.editor.IModelDeltaDecoration[]
|
||||
>([])
|
||||
const [mergeDecorationsCollection, setMergeDecorationsCollection] =
|
||||
useState<monaco.editor.IEditorDecorationsCollection>()
|
||||
|
||||
// Editor state
|
||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||
const [cursorLine, setCursorLine] = useState(0)
|
||||
@ -375,6 +382,48 @@ export default function CodeEditor({
|
||||
})
|
||||
}, [editorRef])
|
||||
|
||||
// handle apply code
|
||||
const handleApplyCode = useCallback(
|
||||
(mergedCode: string, originalCode: string) => {
|
||||
if (!editorRef) return
|
||||
|
||||
const originalLines = originalCode.split("\n")
|
||||
const mergedLines = mergedCode.split("\n")
|
||||
|
||||
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.max(originalLines.length, mergedLines.length);
|
||||
i++
|
||||
) {
|
||||
// Only highlight new lines (green highlights)
|
||||
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
|
||||
decorations.push({
|
||||
range: new monaco.Range(i + 1, 1, i + 1, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: "added-line-decoration",
|
||||
glyphMarginClassName: "added-line-glyph",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Store original content in the model
|
||||
const model = editorRef.getModel()
|
||||
if (model) {
|
||||
;(model as any).originalContent = originalCode
|
||||
}
|
||||
editorRef.setValue(mergedCode)
|
||||
|
||||
// Apply decorations
|
||||
const newDecorations = editorRef.createDecorationsCollection(decorations)
|
||||
setMergeDecorationsCollection(newDecorations)
|
||||
},
|
||||
[editorRef]
|
||||
)
|
||||
|
||||
// Generate widget effect
|
||||
useEffect(() => {
|
||||
if (generate.show) {
|
||||
@ -1087,62 +1136,62 @@ export default function CodeEditor({
|
||||
</div>
|
||||
</>
|
||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{/* {provider && userInfo ? (
|
||||
clerk.loaded ? (
|
||||
<>
|
||||
{/* {provider && userInfo ? (
|
||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||
) : null} */}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? "") // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
// If the new content is different from the cached content, update it
|
||||
if (value !== fileContents[activeFileId]) {
|
||||
setActiveFileContent(value ?? "") // Update the active file content
|
||||
// Mark the file as unsaved by setting 'saved' to false
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: false }
|
||||
: tab
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// If the content matches the cached content, mark the file as saved
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeFileId
|
||||
? { ...tab, saved: true }
|
||||
: tab
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme={theme === "light" ? "vs" : "vs-dark"}
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
tabSize: 2,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
padding: {
|
||||
bottom: 4,
|
||||
top: 4,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
fixedOverflowWidgets: true,
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
}}
|
||||
theme={theme === "light" ? "vs" : "vs-dark"}
|
||||
value={activeFileContent}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||
Waiting for Clerk to load...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
@ -1234,6 +1283,11 @@ export default function CodeEditor({
|
||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||
files={files}
|
||||
templateType={sandboxData.type}
|
||||
projectName={sandboxData.name}
|
||||
handleApplyCode={handleApplyCode}
|
||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
||||
selectFile={selectFile}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
|
@ -7,7 +7,7 @@ import "./xterm.css"
|
||||
import { debounce } from "@/lib/utils"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { ElementRef, useEffect, useRef } from "react"
|
||||
import { ElementRef, useEffect, useRef, useCallback } from "react"
|
||||
import { Socket } from "socket.io-client"
|
||||
export default function EditorTerminal({
|
||||
socket,
|
||||
@ -28,7 +28,6 @@ export default function EditorTerminal({
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalContainerRef.current) return
|
||||
// console.log("new terminal", id, term ? "reusing" : "creating");
|
||||
|
||||
const terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
@ -37,13 +36,54 @@ export default function EditorTerminal({
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
letterSpacing: 0,
|
||||
allowTransparency: true,
|
||||
rightClickSelectsWord: true,
|
||||
allowProposedApi: true, // for custom key events
|
||||
})
|
||||
|
||||
// right-click paste handler
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
if (text) {
|
||||
socket.emit("terminalData", { id, data: text })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
terminalContainerRef.current.addEventListener(
|
||||
"contextmenu",
|
||||
handleContextMenu
|
||||
)
|
||||
|
||||
// keyboard paste handler
|
||||
terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
||||
if (event.type === "keydown") {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key.toLowerCase() === "v"
|
||||
) {
|
||||
event.preventDefault()
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
if (text) {
|
||||
socket.emit("terminalData", { id, data: text })
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
setTerm(terminal)
|
||||
const dispose = () => {
|
||||
|
||||
return () => {
|
||||
terminal.dispose()
|
||||
terminalContainerRef.current?.removeEventListener(
|
||||
"contextmenu",
|
||||
handleContextMenu
|
||||
)
|
||||
}
|
||||
return dispose
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -81,7 +121,6 @@ export default function EditorTerminal({
|
||||
|
||||
const { width, height } = entry.contentRect
|
||||
|
||||
// Only call fit if the size has actually changed
|
||||
if (
|
||||
width !== terminalContainerRef.current.offsetWidth ||
|
||||
height !== terminalContainerRef.current.offsetHeight
|
||||
@ -92,10 +131,9 @@ export default function EditorTerminal({
|
||||
console.error("Error during fit:", err)
|
||||
}
|
||||
}
|
||||
}, 50) // Debounce for 50ms
|
||||
}, 50)
|
||||
)
|
||||
|
||||
// start observing for resize
|
||||
resizeObserver.observe(terminalContainerRef.current)
|
||||
return () => {
|
||||
disposableOnData.dispose()
|
||||
@ -124,6 +162,7 @@ export default function EditorTerminal({
|
||||
ref={terminalContainerRef}
|
||||
style={{ display: visible ? "block" : "none" }}
|
||||
className="w-full h-full text-left"
|
||||
tabIndex={0}
|
||||
>
|
||||
{term === null ? (
|
||||
<div className="flex items-center text-muted-foreground p-2">
|
||||
|
@ -231,7 +231,10 @@ function ProfileCard({
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-xs text-muted-foreground">{joinedAt}</p>
|
||||
{typeof generations === "number" && (
|
||||
<SubscriptionBadge generations={generations} tier={tier as keyof typeof TIERS} />
|
||||
<SubscriptionBadge
|
||||
generations={generations}
|
||||
tier={tier as keyof typeof TIERS}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@ -445,7 +448,13 @@ const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
|
||||
// #endregion
|
||||
|
||||
// #region Sub Badge
|
||||
const SubscriptionBadge = ({ generations, tier = "FREE" }: { generations: number, tier?: keyof typeof TIERS }) => {
|
||||
const SubscriptionBadge = ({
|
||||
generations,
|
||||
tier = "FREE",
|
||||
}: {
|
||||
generations: number
|
||||
tier?: keyof typeof TIERS
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Badge variant="secondary" className="text-sm cursor-pointer">
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { User } from "@/lib/types"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import {
|
||||
Crown,
|
||||
Crown,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
@ -43,7 +43,11 @@ const TIER_INFO = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function UserButton({ userData: initialUserData }: { userData: User }) {
|
||||
export default function UserButton({
|
||||
userData: initialUserData,
|
||||
}: {
|
||||
userData: User
|
||||
}) {
|
||||
const [userData, setUserData] = useState<User>(initialUserData)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { signOut } = useClerk()
|
||||
@ -57,7 +61,7 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
cache: 'no-store'
|
||||
cache: "no-store",
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
@ -75,9 +79,12 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const tierInfo = TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||
const tierInfo =
|
||||
TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||
const TierIcon = tierInfo.icon
|
||||
const usagePercentage = Math.floor((userData.generations || 0) * 100 / tierInfo.limit)
|
||||
const usagePercentage = Math.floor(
|
||||
((userData.generations || 0) * 100) / tierInfo.limit
|
||||
)
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
router.push(`/@${userData.username}`)
|
||||
@ -98,7 +105,6 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link href={"/dashboard"}>
|
||||
<LayoutDashboard className="mr-2 size-4" />
|
||||
@ -114,12 +120,13 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
|
||||
<span className="text-sm font-medium">{userData.tier || "FREE"} Plan</span>
|
||||
<span className="text-sm font-medium">
|
||||
{userData.tier || "FREE"} Plan
|
||||
</span>
|
||||
</div>
|
||||
{(userData.tier === "FREE" || userData.tier === "PRO") && (
|
||||
<Button
|
||||
@ -139,16 +146,20 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>AI Usage</span>
|
||||
<span>{userData.generations}/{tierInfo.limit}</span>
|
||||
<span>
|
||||
{userData.generations}/{tierInfo.limit}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary mb-1">
|
||||
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary mb-1">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
usagePercentage > 90 ? 'bg-red-500' :
|
||||
usagePercentage > 75 ? 'bg-yellow-500' :
|
||||
tierInfo.color.replace('text-', 'bg-')
|
||||
}`}
|
||||
usagePercentage > 90
|
||||
? "bg-red-500"
|
||||
: usagePercentage > 75
|
||||
? "bg-yellow-500"
|
||||
: tierInfo.color.replace("text-", "bg-")
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min(usagePercentage, 100)}%`,
|
||||
}}
|
||||
@ -173,4 +184,3 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,6 @@ export const projectTemplates: {
|
||||
name: "PHP",
|
||||
description: "PHP development environment",
|
||||
icon: "/project-icons/php.svg",
|
||||
disabled: false
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
|
@ -1,290 +1,290 @@
|
||||
export interface TemplateConfig {
|
||||
id: string
|
||||
name: string,
|
||||
runCommand: string,
|
||||
fileStructure: {
|
||||
[key: string]: {
|
||||
purpose: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
conventions: string[]
|
||||
dependencies?: {
|
||||
[key: string]: string
|
||||
}
|
||||
scripts?: {
|
||||
[key: string]: string
|
||||
id: string
|
||||
name: string
|
||||
runCommand: string
|
||||
fileStructure: {
|
||||
[key: string]: {
|
||||
purpose: string
|
||||
description: string
|
||||
}
|
||||
}
|
||||
|
||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||
reactjs: {
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"src/": {
|
||||
purpose: "source",
|
||||
description: "Contains all React components and application logic"
|
||||
},
|
||||
"src/components/": {
|
||||
purpose: "components",
|
||||
description: "Reusable React components"
|
||||
},
|
||||
"src/lib/": {
|
||||
purpose: "utilities",
|
||||
description: "Utility functions and shared code"
|
||||
},
|
||||
"src/App.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Main application component"
|
||||
},
|
||||
"src/index.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application entry point"
|
||||
},
|
||||
"src/index.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and index.html"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"vite.config.ts": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use functional components with hooks",
|
||||
"Follow React naming conventions (PascalCase for components)",
|
||||
"Keep components small and focused",
|
||||
"Use TypeScript for type safety"
|
||||
],
|
||||
dependencies: {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
}
|
||||
},
|
||||
// Next.js template config
|
||||
nextjs: {
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"pages/": {
|
||||
purpose: "routing",
|
||||
description: "Page components and API routes"
|
||||
},
|
||||
"pages/api/": {
|
||||
purpose: "api",
|
||||
description: "API route handlers"
|
||||
},
|
||||
"pages/_app.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application wrapper component"
|
||||
},
|
||||
"pages/index.tsx": {
|
||||
purpose: "page",
|
||||
description: "Homepage component"
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and files"
|
||||
},
|
||||
"styles/": {
|
||||
purpose: "styles",
|
||||
description: "CSS modules and global styles"
|
||||
},
|
||||
"styles/globals.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"styles/Home.module.css": {
|
||||
purpose: "styles",
|
||||
description: "Homepage-specific styles"
|
||||
},
|
||||
"next.config.js": {
|
||||
purpose: "config",
|
||||
description: "Next.js configuration"
|
||||
},
|
||||
"next-env.d.ts": {
|
||||
purpose: "types",
|
||||
description: "Next.js TypeScript declarations"
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use file-system based routing",
|
||||
"Keep API routes in pages/api",
|
||||
"Use CSS Modules for component styles",
|
||||
"Follow Next.js data fetching patterns"
|
||||
],
|
||||
dependencies: {
|
||||
"next": "^14.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "^3.4.1"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
}
|
||||
},
|
||||
// Streamlit template config
|
||||
streamlit: {
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
fileStructure: {
|
||||
"main.py": {
|
||||
purpose: "entry",
|
||||
description: "Main Streamlit application file"
|
||||
},
|
||||
"requirements.txt": {
|
||||
purpose: "dependencies",
|
||||
description: "Python package dependencies"
|
||||
},
|
||||
"Procfile": {
|
||||
purpose: "deployment",
|
||||
description: "Deployment configuration for hosting platforms"
|
||||
},
|
||||
"venv/": {
|
||||
purpose: "environment",
|
||||
description: "Python virtual environment directory"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use Streamlit components for UI",
|
||||
"Follow PEP 8 style guide",
|
||||
"Keep dependencies in requirements.txt",
|
||||
"Use virtual environment for isolation"
|
||||
],
|
||||
dependencies: {
|
||||
"streamlit": "^1.40.0",
|
||||
"altair": "^5.5.0"
|
||||
},
|
||||
scripts: {
|
||||
"start": "streamlit run main.py",
|
||||
"dev": "./venv/bin/streamlit run main.py --server.runOnSave true"
|
||||
}
|
||||
},
|
||||
// HTML template config
|
||||
vanillajs: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"index.html": {
|
||||
purpose: "entry",
|
||||
description: "Main HTML entry point"
|
||||
},
|
||||
"style.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles"
|
||||
},
|
||||
"script.js": {
|
||||
purpose: "scripts",
|
||||
description: "JavaScript application logic"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts"
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions"
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Use semantic HTML elements",
|
||||
"Keep CSS modular and organized",
|
||||
"Write clean, modular JavaScript",
|
||||
"Follow modern ES6+ practices"
|
||||
],
|
||||
dependencies: {
|
||||
"vite": "^5.0.12"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
},
|
||||
// PHP template config
|
||||
php: {
|
||||
id: "php",
|
||||
name: "PHP",
|
||||
runCommand: "npx vite",
|
||||
fileStructure: {
|
||||
"index.php": {
|
||||
purpose: "entry",
|
||||
description: "Main PHP entry point"
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Frontend dependencies and scripts"
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions"
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite configuration for frontend assets"
|
||||
},
|
||||
"node_modules/": {
|
||||
purpose: "dependencies",
|
||||
description: "Frontend dependency files"
|
||||
}
|
||||
},
|
||||
conventions: [
|
||||
"Follow PSR-12 coding standards",
|
||||
"Use modern PHP 8+ features",
|
||||
"Organize assets with Vite",
|
||||
"Keep PHP logic separate from presentation"
|
||||
],
|
||||
dependencies: {
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
scripts: {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
conventions: string[]
|
||||
dependencies?: {
|
||||
[key: string]: string
|
||||
}
|
||||
scripts?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||
reactjs: {
|
||||
id: "reactjs",
|
||||
name: "React",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"src/": {
|
||||
purpose: "source",
|
||||
description: "Contains all React components and application logic",
|
||||
},
|
||||
"src/components/": {
|
||||
purpose: "components",
|
||||
description: "Reusable React components",
|
||||
},
|
||||
"src/lib/": {
|
||||
purpose: "utilities",
|
||||
description: "Utility functions and shared code",
|
||||
},
|
||||
"src/App.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Main application component",
|
||||
},
|
||||
"src/index.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application entry point",
|
||||
},
|
||||
"src/index.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and index.html",
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration",
|
||||
},
|
||||
"vite.config.ts": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use functional components with hooks",
|
||||
"Follow React naming conventions (PascalCase for components)",
|
||||
"Keep components small and focused",
|
||||
"Use TypeScript for type safety",
|
||||
],
|
||||
dependencies: {
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
clsx: "^2.1.1",
|
||||
"lucide-react": "^0.441.0",
|
||||
react: "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "tsc && vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
// Next.js template config
|
||||
nextjs: {
|
||||
id: "nextjs",
|
||||
name: "NextJS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"pages/": {
|
||||
purpose: "routing",
|
||||
description: "Page components and API routes",
|
||||
},
|
||||
"pages/api/": {
|
||||
purpose: "api",
|
||||
description: "API route handlers",
|
||||
},
|
||||
"pages/_app.tsx": {
|
||||
purpose: "entry",
|
||||
description: "Application wrapper component",
|
||||
},
|
||||
"pages/index.tsx": {
|
||||
purpose: "page",
|
||||
description: "Homepage component",
|
||||
},
|
||||
"public/": {
|
||||
purpose: "static",
|
||||
description: "Static assets and files",
|
||||
},
|
||||
"styles/": {
|
||||
purpose: "styles",
|
||||
description: "CSS modules and global styles",
|
||||
},
|
||||
"styles/globals.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"styles/Home.module.css": {
|
||||
purpose: "styles",
|
||||
description: "Homepage-specific styles",
|
||||
},
|
||||
"next.config.js": {
|
||||
purpose: "config",
|
||||
description: "Next.js configuration",
|
||||
},
|
||||
"next-env.d.ts": {
|
||||
purpose: "types",
|
||||
description: "Next.js TypeScript declarations",
|
||||
},
|
||||
"tsconfig.json": {
|
||||
purpose: "config",
|
||||
description: "TypeScript configuration",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use file-system based routing",
|
||||
"Keep API routes in pages/api",
|
||||
"Use CSS Modules for component styles",
|
||||
"Follow Next.js data fetching patterns",
|
||||
],
|
||||
dependencies: {
|
||||
next: "^14.1.0",
|
||||
react: "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
tailwindcss: "^3.4.1",
|
||||
},
|
||||
scripts: {
|
||||
dev: "next dev",
|
||||
build: "next build",
|
||||
start: "next start",
|
||||
lint: "next lint",
|
||||
},
|
||||
},
|
||||
// Streamlit template config
|
||||
streamlit: {
|
||||
id: "streamlit",
|
||||
name: "Streamlit",
|
||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
fileStructure: {
|
||||
"main.py": {
|
||||
purpose: "entry",
|
||||
description: "Main Streamlit application file",
|
||||
},
|
||||
"requirements.txt": {
|
||||
purpose: "dependencies",
|
||||
description: "Python package dependencies",
|
||||
},
|
||||
Procfile: {
|
||||
purpose: "deployment",
|
||||
description: "Deployment configuration for hosting platforms",
|
||||
},
|
||||
"venv/": {
|
||||
purpose: "environment",
|
||||
description: "Python virtual environment directory",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use Streamlit components for UI",
|
||||
"Follow PEP 8 style guide",
|
||||
"Keep dependencies in requirements.txt",
|
||||
"Use virtual environment for isolation",
|
||||
],
|
||||
dependencies: {
|
||||
streamlit: "^1.40.0",
|
||||
altair: "^5.5.0",
|
||||
},
|
||||
scripts: {
|
||||
start: "streamlit run main.py",
|
||||
dev: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||
},
|
||||
},
|
||||
// HTML template config
|
||||
vanillajs: {
|
||||
id: "vanillajs",
|
||||
name: "HTML/JS",
|
||||
runCommand: "npm run dev",
|
||||
fileStructure: {
|
||||
"index.html": {
|
||||
purpose: "entry",
|
||||
description: "Main HTML entry point",
|
||||
},
|
||||
"style.css": {
|
||||
purpose: "styles",
|
||||
description: "Global CSS styles",
|
||||
},
|
||||
"script.js": {
|
||||
purpose: "scripts",
|
||||
description: "JavaScript application logic",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Project dependencies and scripts",
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions",
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite bundler configuration",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Use semantic HTML elements",
|
||||
"Keep CSS modular and organized",
|
||||
"Write clean, modular JavaScript",
|
||||
"Follow modern ES6+ practices",
|
||||
],
|
||||
dependencies: {
|
||||
vite: "^5.0.12",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
// PHP template config
|
||||
php: {
|
||||
id: "php",
|
||||
name: "PHP",
|
||||
runCommand: "npx vite",
|
||||
fileStructure: {
|
||||
"index.php": {
|
||||
purpose: "entry",
|
||||
description: "Main PHP entry point",
|
||||
},
|
||||
"package.json": {
|
||||
purpose: "config",
|
||||
description: "Frontend dependencies and scripts",
|
||||
},
|
||||
"package-lock.json": {
|
||||
purpose: "config",
|
||||
description: "Locked dependency versions",
|
||||
},
|
||||
"vite.config.js": {
|
||||
purpose: "config",
|
||||
description: "Vite configuration for frontend assets",
|
||||
},
|
||||
"node_modules/": {
|
||||
purpose: "dependencies",
|
||||
description: "Frontend dependency files",
|
||||
},
|
||||
},
|
||||
conventions: [
|
||||
"Follow PSR-12 coding standards",
|
||||
"Use modern PHP 8+ features",
|
||||
"Organize assets with Vite",
|
||||
"Keep PHP logic separate from presentation",
|
||||
],
|
||||
dependencies: {
|
||||
vite: "^5.0.0",
|
||||
},
|
||||
scripts: {
|
||||
dev: "vite",
|
||||
build: "vite build",
|
||||
preview: "vite preview",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
export const TIERS = {
|
||||
FREE: {
|
||||
// generations: 100,
|
||||
// maxTokens: 1024,
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
PRO: {
|
||||
generations: 500,
|
||||
maxTokens: 2048,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
ENTERPRISE: {
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
}
|
||||
FREE: {
|
||||
// generations: 100,
|
||||
// maxTokens: 1024,
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
PRO: {
|
||||
generations: 500,
|
||||
maxTokens: 2048,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
ENTERPRISE: {
|
||||
generations: 1000,
|
||||
maxTokens: 4096,
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
},
|
||||
}
|
||||
|
60
frontend/package-lock.json
generated
60
frontend/package-lock.json
generated
@ -54,6 +54,7 @@
|
||||
"monaco-themes": "^0.4.4",
|
||||
"next": "14.1.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.73.1",
|
||||
"posthog-js": "^1.147.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18",
|
||||
@ -7492,6 +7493,65 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.73.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.73.1.tgz",
|
||||
"integrity": "sha512-nWImDJBcUsqrhy7yJScXB4+iqjzbUEgzfA3un/6UnHFdwWhjX24oztj69Ped/njABfOdLcO/F7CeWTI5dt8Xmg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
"version": "18.19.67",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz",
|
||||
"integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ora": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz",
|
||||
|
@ -55,6 +55,7 @@
|
||||
"monaco-themes": "^0.4.4",
|
||||
"next": "14.1.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.73.1",
|
||||
"posthog-js": "^1.147.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18",
|
||||
|
Loading…
x
Reference in New Issue
Block a user