Skip to content
Open
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
31 changes: 31 additions & 0 deletions migrations/Version20260218143103.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260218143103 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE html_editor_builds (id CHAR(36) NOT NULL, workspace_id CHAR(36) NOT NULL, source_path VARCHAR(512) NOT NULL, user_email VARCHAR(255) NOT NULL, status VARCHAR(32) NOT NULL, error_message LONGTEXT DEFAULT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci`');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE html_editor_builds');
}
}
127 changes: 127 additions & 0 deletions src/ChatBasedContentEditor/Domain/Entity/HtmlEditorBuild.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace App\ChatBasedContentEditor\Domain\Entity;

use App\ChatBasedContentEditor\Domain\Enum\HtmlEditorBuildStatus;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use EnterpriseToolingForSymfony\SharedBundle\DateAndTime\Service\DateAndTimeService;
use Exception;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;

#[ORM\Entity]
#[ORM\Table(name: 'html_editor_builds')]
class HtmlEditorBuild
{
/**
* @throws Exception
*/
public function __construct(
string $workspaceId,
string $sourcePath,
string $userEmail
) {
$this->workspaceId = $workspaceId;
$this->sourcePath = $sourcePath;
$this->userEmail = $userEmail;
$this->status = HtmlEditorBuildStatus::Pending;
$this->createdAt = DateAndTimeService::getDateTimeImmutable();
}

#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
#[ORM\Column(
type: Types::GUID,
unique: true
)]
private ?string $id = null;

public function getId(): ?string
{
return $this->id;
}

#[ORM\Column(
type: Types::GUID,
nullable: false
)]
private readonly string $workspaceId;

public function getWorkspaceId(): string
{
return $this->workspaceId;
}

#[ORM\Column(
type: Types::STRING,
length: 512,
nullable: false
)]
private readonly string $sourcePath;

public function getSourcePath(): string
{
return $this->sourcePath;
}

#[ORM\Column(
type: Types::STRING,
length: 255,
nullable: false
)]
private readonly string $userEmail;

public function getUserEmail(): string
{
return $this->userEmail;
}

#[ORM\Column(
type: Types::STRING,
length: 32,
nullable: false,
enumType: HtmlEditorBuildStatus::class
)]
private HtmlEditorBuildStatus $status;

public function getStatus(): HtmlEditorBuildStatus
{
return $this->status;
}

public function setStatus(HtmlEditorBuildStatus $status): void
{
$this->status = $status;
}

#[ORM\Column(
type: Types::TEXT,
nullable: true
)]
private ?string $errorMessage = null;

public function getErrorMessage(): ?string
{
return $this->errorMessage;
}

public function setErrorMessage(?string $errorMessage): void
{
$this->errorMessage = $errorMessage;
}

#[ORM\Column(
type: Types::DATETIME_IMMUTABLE,
nullable: false
)]
private readonly DateTimeImmutable $createdAt;

public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
}
13 changes: 13 additions & 0 deletions src/ChatBasedContentEditor/Domain/Enum/HtmlEditorBuildStatus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\ChatBasedContentEditor\Domain\Enum;

enum HtmlEditorBuildStatus: string
{
case Pending = 'pending';
case Running = 'running';
case Completed = 'completed';
case Failed = 'failed';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace App\ChatBasedContentEditor\Infrastructure\Handler;

use App\ChatBasedContentEditor\Domain\Entity\HtmlEditorBuild;
use App\ChatBasedContentEditor\Domain\Enum\HtmlEditorBuildStatus;
use App\ChatBasedContentEditor\Infrastructure\Message\RunHtmlBuildMessage;
use App\ProjectMgmt\Facade\ProjectMgmtFacadeInterface;
use App\WorkspaceMgmt\Facade\WorkspaceMgmtFacadeInterface;
use App\WorkspaceTooling\Facade\WorkspaceToolingServiceInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Throwable;

#[AsMessageHandler]
final readonly class RunHtmlBuildHandler
{
public function __construct(
private EntityManagerInterface $entityManager,
private WorkspaceMgmtFacadeInterface $workspaceMgmtFacade,
private ProjectMgmtFacadeInterface $projectMgmtFacade,
private WorkspaceToolingServiceInterface $workspaceToolingService,
private LoggerInterface $logger,
) {
}

public function __invoke(RunHtmlBuildMessage $message): void
{
$build = $this->entityManager->find(HtmlEditorBuild::class, $message->buildId);

if ($build === null) {
$this->logger->error('HtmlEditorBuild not found', ['buildId' => $message->buildId]);

return;
}

$build->setStatus(HtmlEditorBuildStatus::Running);
$this->entityManager->flush();

try {
$workspace = $this->workspaceMgmtFacade->getWorkspaceById($build->getWorkspaceId());

if ($workspace === null) {
throw new RuntimeException('Workspace not found: ' . $build->getWorkspaceId());
}

$project = $this->projectMgmtFacade->getProjectInfo($workspace->projectId);

$buildOutput = $this->workspaceToolingService->runBuildInWorkspace(
$workspace->workspacePath,
$project->agentImage
);

$this->logger->info('HTML editor Docker build completed', [
'buildId' => $message->buildId,
'workspaceId' => $build->getWorkspaceId(),
'sourcePath' => $build->getSourcePath(),
'buildOutput' => $buildOutput,
]);

$this->workspaceMgmtFacade->commitAndPush(
$build->getWorkspaceId(),
'Manual HTML edit: ' . $build->getSourcePath(),
$build->getUserEmail()
);

$build->setStatus(HtmlEditorBuildStatus::Completed);
$this->entityManager->flush();
} catch (Throwable $e) {
$this->logger->error('HTML editor build failed', [
'buildId' => $message->buildId,
'workspaceId' => $build->getWorkspaceId(),
'sourcePath' => $build->getSourcePath(),
'error' => $e->getMessage(),
]);

$build->setStatus(HtmlEditorBuildStatus::Failed);
$build->setErrorMessage($e->getMessage());
$this->entityManager->flush();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace App\ChatBasedContentEditor\Infrastructure\Message;

use EnterpriseToolingForSymfony\SharedBundle\WorkerSystem\SymfonyMessage\ImmediateSymfonyMessageInterface;

readonly class RunHtmlBuildMessage implements ImmediateSymfonyMessageInterface
{
public function __construct(
public string $buildId,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use App\ChatBasedContentEditor\Domain\Entity\Conversation;
use App\ChatBasedContentEditor\Domain\Entity\EditSession;
use App\ChatBasedContentEditor\Domain\Entity\EditSessionChunk;
use App\ChatBasedContentEditor\Domain\Entity\HtmlEditorBuild;
use App\ChatBasedContentEditor\Domain\Enum\ConversationStatus;
use App\ChatBasedContentEditor\Domain\Enum\EditSessionStatus;
use App\ChatBasedContentEditor\Domain\Service\ConversationService;
use App\ChatBasedContentEditor\Infrastructure\Adapter\DistFileScannerInterface;
use App\ChatBasedContentEditor\Infrastructure\Message\RunEditSessionMessage;
use App\ChatBasedContentEditor\Infrastructure\Message\RunHtmlBuildMessage;
use App\ChatBasedContentEditor\Presentation\Service\ConversationContextUsageService;
use App\ChatBasedContentEditor\Presentation\Service\PromptSuggestionsService;
use App\LlmContentEditor\Facade\Dto\AgentConfigDto;
Expand All @@ -24,6 +26,7 @@
use App\WorkspaceMgmt\Facade\Enum\WorkspaceStatus;
use App\WorkspaceMgmt\Facade\WorkspaceMgmtFacadeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -60,6 +63,7 @@ public function __construct(
private readonly TranslatorInterface $translator,
private readonly PromptSuggestionsService $promptSuggestionsService,
private readonly LlmContentEditorFacadeInterface $llmContentEditorFacade,
private readonly LoggerInterface $logger,
) {
}

Expand Down Expand Up @@ -818,25 +822,61 @@ public function savePage(
// Write the file to src/
$this->workspaceMgmtFacade->writeWorkspaceFile($workspaceId, $sourcePath, $content);

// Run build to update dist/ from src/
$this->workspaceMgmtFacade->runBuild($workspaceId);

// Get user email for commit author
$accountInfo = $this->getAccountInfo($user);

// Commit and push the changes (now includes both src/ and rebuilt dist/)
$this->workspaceMgmtFacade->commitAndPush(
$workspaceId,
'Manual HTML edit: ' . $sourcePath,
$accountInfo->email
);
// Create a build record and dispatch async build via Messenger.
// The messenger container has Docker socket access required for
// running the build in the project's agent image.
$build = new HtmlEditorBuild($workspaceId, $sourcePath, $accountInfo->email);
$this->entityManager->persist($build);
$this->entityManager->flush();

$buildId = $build->getId();
if ($buildId === null) {
return $this->json(['error' => 'Failed to create build record.'], Response::HTTP_INTERNAL_SERVER_ERROR);
}

$this->messageBus->dispatch(new RunHtmlBuildMessage($buildId));

return $this->json(['success' => true]);
$this->logger->info('HTML editor build dispatched', [
'buildId' => $buildId,
'workspaceId' => $workspaceId,
'sourcePath' => $sourcePath,
]);

return $this->json(['buildId' => $buildId], Response::HTTP_ACCEPTED);
} catch (Throwable $e) {
$this->logger->error('HTML editor save failed', [
'workspaceId' => $workspaceId,
'sourcePath' => $sourcePath,
'error' => $e->getMessage(),
]);

return $this->json(['error' => 'Failed to save file: ' . $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}

#[Route(
path: '/workspace/{workspaceId}/build-status/{buildId}',
name: 'chat_based_content_editor.presentation.build_status',
methods: [Request::METHOD_GET],
requirements: ['workspaceId' => '[a-f0-9-]{36}', 'buildId' => '[a-f0-9-]{36}']
)]
public function pollBuildStatus(string $workspaceId, string $buildId): Response
{
$build = $this->entityManager->find(HtmlEditorBuild::class, $buildId);

if ($build === null || $build->getWorkspaceId() !== $workspaceId) {
return $this->json(['error' => 'Build not found.'], Response::HTTP_NOT_FOUND);
}

return $this->json([
'status' => $build->getStatus()->value,
'error' => $build->getErrorMessage(),
]);
}

/**
* Map a dist/ path to the corresponding src/ path.
*
Expand Down
Loading