repository = new ContentRepository();
$this->collectionRepository = new CollectionRepository();
$this->collectionValidator = new CollectionValidator($this->collectionRepository);
$this->generateUseCase = new GenerateContentUseCase();
}
/**
* GET /content
* List all content orders
*/
public function index(): void
{
$filters = [];
$status = $this->getString('status');
if ($status !== '') {
$filters['status'] = $status;
}
$orders = $this->repository->findAllOrders($filters);
$stats = $this->repository->getStatistics();
$this->view('content.index', [
'title' => 'Content Studio',
'orders' => $orders,
'stats' => $stats,
'currentStatus' => $status,
]);
}
/**
* GET /content/new
* Show create form
*/
public function contentNew(): void
{
$collections = $this->getAvailableCollections();
$lastSettings = $this->repository->getLastOrderSettings();
$this->view('content.new', [
'title' => 'Neuer Content-Auftrag',
'profiles' => $this->repository->findAllProfiles(),
'contracts' => $this->repository->findAllContracts(),
'structures' => $this->repository->findAllStructures(),
'models' => ModelConfig::getAll(),
'collections' => $collections,
// Defaults from last order
'defaultModel' => $lastSettings['model'],
'defaultCollections' => $lastSettings['collections'],
'defaultContextLimit' => $lastSettings['context_limit'],
'defaultProfileId' => $lastSettings['author_profile_id'],
'defaultContractId' => $lastSettings['contract_id'],
'defaultStructureId' => $lastSettings['structure_id'],
]);
}
/**
* POST /content
* Store new order
*/
public function store(): void
{
$this->requireCsrf();
$command = CreateContentOrderCommand::fromRequest($_POST);
$errors = $command->validate();
if ($errors !== []) {
$_SESSION['error'] = implode(' ', $errors);
$this->redirect('/content/new');
}
// Auto-apply first active contract if none selected
$contractId = $command->contractId;
if ($contractId === null) {
$contracts = $this->repository->findAllContracts();
if ($contracts !== []) {
$contractId = (int) $contracts[0]['id'];
}
}
// Validate collection compatibility
$collections = $this->validateCollections($command->collections);
$compatibility = $this->validateCollectionCompatibility($collections);
if (!$compatibility['valid']) {
$_SESSION['error'] = 'Collection-Fehler: ' . $compatibility['error'];
$this->redirect('/content/new');
}
$model = ModelConfig::validate($command->model);
$orderId = $this->repository->createOrder([
'title' => $command->title,
'briefing' => $command->briefing,
'author_profile_id' => $command->authorProfileId,
'contract_id' => $contractId,
'structure_id' => $command->structureId,
'model' => $model,
'collections' => json_encode($collections),
'context_limit' => $command->contextLimit,
]);
// If "generate" action: generate content immediately
if ($command->shouldGenerate()) {
$collection = $collections[0] ?? 'documents';
$result = $this->generateUseCase->generate($orderId, $model, $collection, $command->contextLimit);
if ($result->hasError()) {
$_SESSION['error'] = 'Generierung fehlgeschlagen: ' . $result->getError();
} else {
$_SESSION['success'] = 'Content wurde generiert.';
}
}
$this->redirect('/content/' . $orderId);
}
/**
* GET /content/{id}
* Show order details
*/
public function show(int $id): void
{
$order = $this->repository->findOrder($id);
if ($order === null) {
$this->notFound('Auftrag nicht gefunden');
}
$versions = $this->repository->findVersionsByOrder($id);
$latestVersion = $versions[0] ?? null;
$critiques = $latestVersion ? $this->repository->findCritiquesByVersion($latestVersion['id']) : [];
$sources = $this->repository->findSourcesByOrder($id);
// Get available collections for the dropdown
$availableCollections = $this->getAvailableCollections();
$this->view('content.show', [
'title' => $order['title'],
'order' => $order,
'versions' => $versions,
'latestVersion' => $latestVersion,
'critiques' => $critiques,
'sources' => $sources,
'models' => ModelConfig::getAll(),
'availableCollections' => $availableCollections,
]);
}
/**
* GET /content/{id}/edit
* Show edit form
*/
public function edit(int $id): void
{
$order = $this->repository->findOrder($id);
if ($order === null) {
$this->notFound('Auftrag nicht gefunden');
}
$this->view('content.edit', [
'title' => 'Auftrag bearbeiten',
'order' => $order,
'profiles' => $this->repository->findAllProfiles(),
'contracts' => $this->repository->findAllContracts(),
'structures' => $this->repository->findAllStructures(),
]);
}
/**
* POST /content/{id}/generate
* Generate content (HTMX)
*/
public function generate(int $id): void
{
$this->requireCsrf();
$command = GenerateContentCommand::fromRequest($id, $_POST);
$errors = $command->validate();
if ($errors !== []) {
$this->htmxError(implode(' ', $errors));
return;
}
// Validate collection
$collections = $this->validateCollections([$command->collection]);
if (empty($collections)) {
$this->htmxError('Ungültige Collection: ' . $command->collection);
return;
}
$collection = $collections[0];
// Validate compatibility (single collection always valid, but check exists)
$compatibility = $this->validateCollectionCompatibility($collections);
if (!$compatibility['valid']) {
$this->htmxError($compatibility['error'] ?? 'Collection-Fehler');
return;
}
$result = $this->generateUseCase->generate($id, $command->model, $collection, $command->contextLimit);
if ($result->hasError()) {
$this->htmxError('Fehler: ' . $result->getError());
return;
}
// Return updated content section
$this->renderVersionPartial($result->toArray());
}
/**
* POST /content/{id}/critique
* Run critique round (HTMX)
*/
public function critique(int $id): void
{
$this->requireCsrf();
// Get latest version
$version = $this->repository->findLatestVersion($id);
if ($version === null) {
$this->htmxError('Keine Version vorhanden.');
return;
}
$model = $_POST['model'] ?? 'claude-opus-4-5-20251101';
$result = $this->generateUseCase->critique($version['id'], $model);
if ($result->hasError()) {
$this->htmxError('Fehler: ' . $result->getError());
return;
}
// Return critique results
$this->renderCritiquePartial($result->toArray());
}
/**
* POST /content/{id}/revise
* Create revision (HTMX)
*/
public function revise(int $id): void
{
$this->requireCsrf();
$version = $this->repository->findLatestVersion($id);
if ($version === null) {
$this->htmxError('Keine Version vorhanden.');
return;
}
$model = $_POST['model'] ?? 'claude-opus-4-5-20251101';
$result = $this->generateUseCase->revise($version['id'], $model);
if ($result->hasError()) {
$this->htmxError('Fehler: ' . $result->getError());
return;
}
$this->renderVersionPartial($result->toArray());
}
/**
* POST /content/{id}/approve
* Approve content
*/
public function approve(int $id): void
{
$this->requireCsrf();
$this->repository->updateOrderStatus($id, 'approve');
$this->htmxSuccess('Content genehmigt!');
echo '';
}
/**
* POST /content/{id}/decline
* Decline content
*/
public function decline(int $id): void
{
$this->requireCsrf();
$this->repository->updateOrderStatus($id, 'draft');
$this->htmxAlert('warning', 'Content abgelehnt. Zurück zu Entwurf.');
echo '';
}
/**
* Render version partial
*/
private function renderVersionPartial(array $result): void
{
$this->view('content.partials.version', [
'content' => $result['content'] ?? '',
'sources' => $result['sources'] ?? [],
'versionNumber' => $result['version_number'] ?? '?',
]);
}
/**
* Render critique partial
*/
private function renderCritiquePartial(array $result): void
{
$this->view('content.partials.critique', [
'critiques' => $result['critiques'] ?? [],
'allPassed' => $result['all_passed'] ?? false,
'round' => $result['round'] ?? '?',
]);
}
/**
* Get available collections from database
*
* @return array>
*/
private function getAvailableCollections(): array
{
$collections = $this->collectionRepository->getSearchable();
if ($collections === []) {
return [
['collection_id' => 'documents', 'display_name' => 'Dokumente', 'points_count' => 0, 'vector_size' => 1024],
];
}
return $collections;
}
/**
* Validate collections against available ones
*
* @param array $collections
* @return array
*/
private function validateCollections(array $collections): array
{
$availableIds = array_column($this->getAvailableCollections(), 'collection_id');
$valid = array_filter($collections, fn ($c) => in_array($c, $availableIds, true));
return array_values($valid);
}
/**
* Validate collection compatibility (vector dimensions)
*
* @param array $collectionIds
* @return array{valid: bool, error: string|null}
*/
private function validateCollectionCompatibility(array $collectionIds): array
{
if (empty($collectionIds)) {
return ['valid' => true, 'error' => null];
}
$result = $this->collectionValidator->validateSelection($collectionIds);
return [
'valid' => $result->isValid(),
'error' => $result->getError(),
];
}
}