Compare commits

...

11 Commits

Author SHA1 Message Date
Akhileshrangani4
f6fee20955 chore: update .env.example 2024-12-06 14:24:06 -05:00
Akhileshrangani4
74b0c6b9e4 fix: apply code- discard button 2024-12-01 22:15:03 -05:00
Akhileshrangani4
d840aad3e9 feat: Add clickable file paths in AI chat responses
- Detect file paths with dots in directory names (e.g. next/styles/SignIn.module.css)
- Create new files when path ends with "(new file)"
- Use existing socket connection and file management system
2024-12-01 18:52:02 -05:00
Akhileshrangani4
89804c5906 feat: file path above each code snippet 2024-12-01 14:28:48 -05:00
Akhileshrangani4
aa33ad3031 feat: introduce apply button functionality (v0.1)
### Summary
- Added a new "Apply" button to code snippets provided by the AI assistant.
- The button is designed to seamlessly merge the AI-generated snippet into the relevant file in the editor.

### Current Issues
1. **Sticky Accept/Decline Buttons:** These activate for every snippet instead of being limited to the relevant snippet.
2. **Discard Button:** Currently non-functional.
3. **Highlight Inconsistencies:** The green-red code highlights for old and new code are inconsistent.

### To Do
- Implement a toast notification when the "Apply" button is pressed on an irrelevant tab to prevent code application errors.

### Workflow Implemented
1. The "Apply" button is added alongside "Copy" and "Reply" for AI-generated code snippets.
2. Upon clicking "Apply," the code snippet and relevant file content (active file) are sent to a secondary model (GPT-4O).
3. The system prompt for GPT-4O instructs it to merge the snippet with the file content:
   - Ensure the original file functionality remains intact.
   - Integrate the code snippet seamlessly.
4. The output from GPT-4O is injected directly into the code editor.
5. Changes are visually highlighted:
   - Green for new code.
   - Red for removed code.
6. Highlights remain until the user explicitly accepts or discards the changes.
2024-11-30 20:56:56 -05:00
Akhileshrangani4
f79115974c feat: add dynamic file structure context in AI chat
- Improved file structure formatting with tree-like visualization
- Added filtering for ignored files and folders
- Added scripts section to template context
- Fixed folder hierarchy display with proper indentation
- Maintains sorting with folders first, then files alphabetically
- Now uses actual project files instead of template structure

Example output:
├── app/
│   ├── api/
│   └── page.tsx
├── components/
└── package.json
2024-11-30 15:53:17 -05:00
Akhileshrangani4
1bb518b1af chore: context tab updates
- Context tab updates with the latest file changes and will not be removed from context when a message is sent
2024-11-30 02:26:34 -05:00
Akhileshrangani4
721ea5cad2 feat: sticky copy-reply button on chat code-snippets 2024-11-30 01:33:54 -05:00
Akhileshrangani4
ebfde291ad fix: terminal paste functionality 2024-11-30 00:03:51 -05:00
Akhileshrangani4
07ca7dee46 fix: image handling in context 2024-11-29 21:49:44 -05:00
Akhileshrangani4
5af35f6920 chore: formatting the code of recent changes 2024-11-29 13:06:12 -05:00
24 changed files with 1613 additions and 985 deletions

View File

@ -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
},
}

View File

@ -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

View File

@ -7,7 +7,6 @@ PORT=4000
WORKERS_KEY=
DATABASE_WORKER_URL=
STORAGE_WORKER_URL=
AI_WORKER_URL=
E2B_API_KEY=
DOKKU_HOST=
DOKKU_USERNAME=

View File

@ -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=

View File

@ -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 }
)
}
}
}

View 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 }
)
}
}

View File

@ -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 }
)
}
}
}

View File

@ -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;
}

View 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>
)
}

View File

@ -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
)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 }) =>

View File

@ -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

View File

@ -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,
}),
})

View File

@ -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>
</>

View File

@ -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">

View File

@ -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">

View File

@ -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>
)
}

View File

@ -38,6 +38,6 @@ export const projectTemplates: {
name: "PHP",
description: "PHP development environment",
icon: "/project-icons/php.svg",
disabled: false
disabled: false,
},
]

View File

@ -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",
},
},
}

View File

@ -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",
},
}

View File

@ -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",

View File

@ -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",