Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .tmuxinator/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,9 @@ windows:
- llm:
- echo "LLM Server (Ctrl+a 3 to focus, Ctrl+a r to restart)"
- pnpm dev
- docker:
root: <%= ENV["PWD"] %>
panes:
- docker:
- echo "Docker Services (Ctrl+a 4 to focus)"
- docker compose up
Binary file removed backend/database.db
Binary file not shown.
2 changes: 1 addition & 1 deletion backend/src/project/project.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ export class Project extends SystemBaseModel {
lazy: true, // Load chats only when accessed
onDelete: 'CASCADE', // Delete chats when user is deleted
})
chats: Chat[];
chats: Promise<Chat[]>;
}
2 changes: 1 addition & 1 deletion backend/src/project/project.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,6 @@ export class ProjectsResolver {
@ResolveField('chats', () => [Chat])
async getChats(@Parent() project: Project): Promise<Chat[]> {
const { chats } = await this.projectsService.getProjectById(project.id);
return chats?.filter((chat) => !chat.isDeleted) || [];
return (await chats)?.filter((chat) => !chat.isDeleted) || [];
}
}
65 changes: 35 additions & 30 deletions backend/src/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,26 @@ export class ProjectService {
});

if (projects && projects.length > 0) {
projects.forEach((project) => {
// Filter deleted packages
project.projectPackages = project.projectPackages.filter(
(pkg) => !pkg.isDeleted,
);
// Filter deleted chats
if (project.chats) {
project.chats = project.chats.filter((chat) => !chat.isDeleted);
}
});
await Promise.all(
projects.map(async (project) => {
// Filter deleted packages
project.projectPackages = project.projectPackages.filter(
(pkg) => !pkg.isDeleted,
);
// Filter deleted chats
if (project.chats) {
const chats = await project.chats;
this.logger.log('Project chats:', chats);
// Create a new Promise that resolves to filtered chats
project.chats = Promise.resolve(
chats.filter((chat) => !chat.isDeleted),
);
}
}),
);
}

if (!projects || projects.length === 0) {
throw new NotFoundException(`User with ID ${userId} has no projects.`);
}
return projects;
return projects.length > 0 ? projects : [];
}

async getProjectById(projectId: string): Promise<Project> {
Expand All @@ -70,18 +74,20 @@ export class ProjectService {
relations: ['projectPackages', 'chats', 'user'],
});

if (project) {
project.projectPackages = project.projectPackages.filter(
(pkg) => !pkg.isDeleted,
);
if (project.chats) {
project.chats = project.chats.filter((chat) => !chat.isDeleted);
}
}

if (!project) {
throw new NotFoundException(`Project with ID ${projectId} not found.`);
}

project.projectPackages = project.projectPackages.filter(
(pkg) => !pkg.isDeleted,
);

if (project.chats) {
const chats = await project.chats;
this.logger.log('Project chats:', chats);
project.chats = Promise.resolve(chats.filter((chat) => !chat.isDeleted));
}

return project;
}

Expand All @@ -95,13 +101,12 @@ export class ProjectService {
}
try {
chat.project = project;
if (!project.chats) {
project.chats = [];
}
const chatArray = await project.chats;
chatArray.push(chat);
console.log(chat);
console.log(project);

// Get current chats and add new chat
const currentChats = await project.chats;
project.chats = Promise.resolve([...currentChats, chat]);

// Save both entities
await this.projectsRepository.save(project);
await this.chatRepository.save(chat);

Expand Down
21 changes: 12 additions & 9 deletions frontend/src/app/(main)/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,20 @@ export default function Home() {
setMessages={setMessages}
/>
</ResizablePanel>

<ResizableHandle withHandle className="hidden md:flex" />

<ResizablePanel
defaultSize={50}
minSize={20}
maxSize={80}
className="h-full overflow-auto"
>
<CodeEngine chatId={chatId} />
</ResizablePanel>
{chatId && (
<ResizablePanel
defaultSize={50}
minSize={20}
maxSize={80}
className="h-full overflow-auto"
>
<div className="p-4">
<CodeEngine chatId={chatId} />
</div>
</ResizablePanel>
)}
</ResizablePanelGroup>
);
}
69 changes: 60 additions & 9 deletions frontend/src/app/api/runProject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,43 @@ function findAvailablePort(
});
}

async function checkExistingContainer(
projectPath: string
): Promise<string | null> {
return new Promise((resolve) => {
const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase();
exec(
`docker ps --filter "label=traefik.http.routers.${subdomain}.rule" --format "{{.ID}}"`,
(error, stdout) => {
if (error || !stdout.trim()) {
resolve(null);
} else {
resolve(stdout.trim());
}
}
);
});
}

async function buildAndRunDocker(
projectPath: string
): Promise<{ domain: string; containerId: string }> {
console.log(runningContainers);
if (runningContainers.has(projectPath)) {
console.log(`Container for project ${projectPath} is already running.`);
return runningContainers.get(projectPath)!;
}
const traefikDomain = process.env.TRAEFIK_DOMAIN || 'docker.localhost';
const directory = path.join(getProjectPath(projectPath), 'frontend');
const imageName = projectPath.toLowerCase();
const containerId = crypto.randomUUID();
const containerName = `container-${containerId}`;

const existingContainerId = await checkExistingContainer(projectPath);
if (existingContainerId) {
const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase();
const domain = `${subdomain}.${traefikDomain}`;
runningContainers.set(projectPath, {
domain,
containerId: existingContainerId,
});
return { domain, containerId: existingContainerId };
}
const directory = path.join(getProjectPath(projectPath), 'frontend');
const subdomain = projectPath.replace(/[^\w-]/g, '').toLowerCase();
const imageName = subdomain;
const containerName = `container-${subdomain}`;
const domain = `${subdomain}.${traefikDomain}`;
const exposedPort = await findAvailablePort();
return new Promise((resolve, reject) => {
Expand All @@ -80,6 +102,11 @@ async function buildAndRunDocker(

exec(runCommand, (runErr, runStdout, runStderr) => {
if (runErr) {
// Check if error is due to container already existing
if (runStderr.includes('Conflict. The container name')) {
resolve({ domain, containerId: containerName });
return;
}
console.error(`Error during Docker run: ${runStderr}`);
return reject(runErr);
}
Expand All @@ -100,6 +127,8 @@ async function buildAndRunDocker(
);
});
}
const processingRequests = new Set<string>();

export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const projectPath = searchParams.get('projectPath');
Expand All @@ -111,6 +140,26 @@ export async function GET(req: Request) {
);
}

// First check if container is already running
const existingContainer = runningContainers.get(projectPath);
if (existingContainer) {
return NextResponse.json({
message: 'Docker container already running',
domain: existingContainer.domain,
containerId: existingContainer.containerId,
});
}

// If already processing this project, don't start another build
if (processingRequests.has(projectPath)) {
return NextResponse.json({
message: 'Build in progress',
status: 'pending',
});
}

processingRequests.add(projectPath);

try {
const { domain, containerId } = await buildAndRunDocker(projectPath);
return NextResponse.json({
Expand All @@ -123,5 +172,7 @@ export async function GET(req: Request) {
{ error: error.message || 'Failed to start Docker container' },
{ status: 500 }
);
} finally {
processingRequests.delete(projectPath);
}
}
Loading
Loading