getString('status'); $this->view('content.index', [ 'title' => 'Content Studio', 'orders' => $this->repository->findAllOrders($status !== '' ? ['status' => $status] : []), 'stats' => $this->repository->getStatistics(), 'currentStatus' => $status, ]); } /** * GET /content/new - Show create form */ public function contentNew(): void { $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' => $this->modelRegistry->getChatModels(), 'collections' => $this->collectionService->getAvailable(), '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'); } // Validate collections $result = $this->collectionService->validateWithCompatibility($command->collections); if (!$result['valid']) { $_SESSION['error'] = 'Collection-Fehler: ' . $result['error']; $this->redirect('/content/new'); } $orderId = $this->repository->createOrder([ 'title' => $command->title, 'briefing' => $command->briefing, 'author_profile_id' => $command->authorProfileId, 'contract_id' => $command->contractId ?? $this->getFirstContractId(), 'structure_id' => $command->structureId, 'model' => $this->modelRegistry->isValid($command->model) ? $command->model : $this->modelRegistry->getDefaultChatModel(), 'collections' => json_encode($result['collections']), 'context_limit' => $command->contextLimit, ]); if ($command->shouldGenerate()) { // Start async generation (non-blocking) $this->repository->updateGenerationStatus($orderId, 'generating'); $this->generateUseCase->generateAsync( $orderId, $this->modelRegistry->isValid($command->model) ? $command->model : $this->modelRegistry->getDefaultChatModel(), $result['collections'][0] ?? 'documents', $command->contextLimit ); } $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; $this->view('content.show', [ 'title' => $order['title'], 'order' => $order, 'versions' => $versions, 'latestVersion' => $latestVersion, 'critiques' => $latestVersion ? $this->repository->findCritiquesByVersion($latestVersion['id']) : [], 'sources' => $this->repository->findSourcesByOrder($id), 'models' => $this->modelRegistry->getChatModels(), 'availableCollections' => $this->collectionService->getAvailable(), ]); } /** * 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 - Start async content generation (HTMX) */ public function generate(int $id): void { $this->requireCsrf(); $command = GenerateContentCommand::fromRequest($id, $_POST); if (($errors = $command->validate()) !== []) { $this->htmxError(implode(' ', $errors)); return; } $result = $this->collectionService->validateWithCompatibility([$command->collection]); if (!$result['valid']) { $this->htmxError($result['error'] ?? 'Collection-Fehler'); return; } // Set status to generating and start async $this->repository->updateGenerationStatus($id, 'generating'); $this->generateUseCase->generateAsync($id, $command->model, $result['collections'][0], $command->contextLimit); // Return polling partial $this->view('content.partials.generating', ['orderId' => $id]); } /** * GET /content/{id}/generation-status - Poll generation status (HTMX) */ public function generationStatus(int $id): void { $order = $this->repository->findOrder($id); if ($order === null) { $this->htmxError('Auftrag nicht gefunden'); return; } $status = $order['generation_status'] ?? 'idle'; if ($status === 'completed') { $version = $this->repository->findLatestVersion($id); if ($version !== null) { // Reset status to idle after showing result $this->repository->updateGenerationStatus($id, 'idle'); $this->renderVersionPartial([ 'content' => $version['content'], 'sources' => $this->repository->findSourcesByOrder($id), 'version_number' => $version['version_number'], ]); return; } } if ($status === 'failed') { // Reset status after showing error $error = $order['generation_error'] ?? 'Unbekannter Fehler'; $this->repository->updateGenerationStatus($id, 'idle'); $this->htmxError('Generierung fehlgeschlagen: ' . $error); return; } if ($status === 'generating' || $status === 'queued') { // Still generating - return polling partial with log $this->view('content.partials.generating', [ 'orderId' => $id, 'log' => $order['generation_log'] ?? '', 'step' => $order['generation_step'] ?? '', ]); return; } // Status is idle - return empty (nothing happening) $this->html(''); } /** * POST /content/{id}/critique - Start async critique round (HTMX) */ public function critique(int $id): void { $this->requireCsrf(); $version = $this->repository->findLatestVersion($id); if ($version === null) { $this->htmxError('Keine Version vorhanden.'); return; } // Set status to critiquing and start async $this->repository->updateCritiqueStatus($id, 'critiquing'); $this->generateUseCase->critiqueAsync($id, $version['id'], $_POST['model'] ?? 'claude-opus-4-5-20251101'); // Return polling partial $this->view('content.partials.critiquing-live', ['orderId' => $id]); } /** * GET /content/{id}/critique-status - Poll critique status (HTMX) */ public function critiqueStatus(int $id): void { $order = $this->repository->findOrder($id); if ($order === null) { $this->htmxError('Auftrag nicht gefunden'); return; } $status = $order['critique_status'] ?? 'idle'; if ($status === 'completed') { // Get latest version and its critiques $version = $this->repository->findLatestVersion($id); if ($version !== null) { $critiques = $this->repository->findCritiquesByVersion($version['id']); // Get the latest round $latestRound = $order['current_critique_round'] ?? 1; // Filter critiques for latest round $roundCritiques = array_filter($critiques, fn ($c) => ($c['round'] ?? 0) === $latestRound); // Reset status to idle after showing result $this->repository->updateCritiqueStatus($id, 'idle'); // Check if all passed $allPassed = true; foreach ($roundCritiques as $c) { if (!($c['passed'] ?? false)) { $allPassed = false; break; } } $this->renderCritiquePartial([ 'critiques' => array_values($roundCritiques), 'all_passed' => $allPassed, 'round' => $latestRound, ]); return; } } if ($status === 'failed') { $error = $order['critique_error'] ?? 'Unbekannter Fehler'; $this->repository->updateCritiqueStatus($id, 'idle'); $this->htmxError('Kritik fehlgeschlagen: ' . $error); return; } if ($status === 'critiquing') { // Still critiquing - return polling partial with log $this->view('content.partials.critiquing-live', [ 'orderId' => $id, 'log' => $order['critique_log'] ?? '', 'step' => $order['critique_step'] ?? '', ]); return; } // Status is idle - return empty $this->html(''); } /** * 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; } $result = $this->generateUseCase->revise($version['id'], $_POST['model'] ?? 'claude-opus-4-5-20251101'); 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!'); $this->html(''); } /** * 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.'); $this->html(''); } private function getFirstContractId(): ?int { $contracts = $this->repository->findAllContracts(); return $contracts !== [] ? (int) $contracts[0]['id'] : null; } private function renderVersionPartial(array $result): void { $this->view('content.partials.version', [ 'content' => $result['content'] ?? '', 'sources' => $result['sources'] ?? [], 'versionNumber' => $result['version_number'] ?? '?', ]); } private function renderCritiquePartial(array $result): void { $this->view('content.partials.critique', [ 'critiques' => $result['critiques'] ?? [], 'allPassed' => $result['all_passed'] ?? false, 'round' => $result['round'] ?? '?', ]); } }