implemented step 12 by Gemini
- Task actions
This commit is contained in:
@ -64,6 +64,7 @@ try {
|
|||||||
|
|
||||||
// Router
|
// Router
|
||||||
$projectActions = new \App\Actions\ProjectActions();
|
$projectActions = new \App\Actions\ProjectActions();
|
||||||
|
$taskActions = new \App\Actions\TaskActions();
|
||||||
$consentService = new \App\Services\ConsentService();
|
$consentService = new \App\Services\ConsentService();
|
||||||
|
|
||||||
switch ($action) {
|
switch ($action) {
|
||||||
@ -130,6 +131,14 @@ try {
|
|||||||
sendResponse(true, $result);
|
sendResponse(true, $result);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'generateWebsite':
|
||||||
|
$projectId = $data['project_id'] ?? null;
|
||||||
|
if (!$projectId) {
|
||||||
|
sendResponse(false, ['code' => 'MISSING_PROJECT_ID', 'message' => 'Project ID is required.'], 400);
|
||||||
|
}
|
||||||
|
sendResponse(true, $taskActions->generateWebsite($userId, $projectId));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
sendResponse(false, ['code' => 'UNKNOWN_ACTION', 'message' => "Action '$action' is not defined."], 404);
|
sendResponse(false, ['code' => 'UNKNOWN_ACTION', 'message' => "Action '$action' is not defined."], 404);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -482,6 +482,33 @@ textarea:focus, input[type="text"]:focus, input[type="email"]:focus, input[type=
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Generation Loader */
|
||||||
|
.generation-loader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border: 4px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#generation-status {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@ -307,9 +307,11 @@
|
|||||||
<!-- Step 6: Generovanie -->
|
<!-- Step 6: Generovanie -->
|
||||||
<div class="step" data-step="6">
|
<div class="step" data-step="6">
|
||||||
<h2>Generovanie obsahu</h2>
|
<h2>Generovanie obsahu</h2>
|
||||||
<p class="step-description">Naša AI teraz pripravuje váš textový obsah.</p>
|
<p class="step-description">Naša AI teraz pripravuje váš textový obsah a štruktúru webu. Môže to trvať niekoľko desiatok sekúnd.</p>
|
||||||
<div class="form-placeholder">
|
|
||||||
<p>Obsah pre krok 6...</p>
|
<div class="generation-loader">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p id="generation-status">Čakám na zaradenie do fronty...</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,13 @@ const App = {
|
|||||||
this.state.currentStep = this.state.project.current_step || 1;
|
this.state.currentStep = this.state.project.current_step || 1;
|
||||||
console.log('Loaded existing project:', this.state.project.project_id);
|
console.log('Loaded existing project:', this.state.project.project_id);
|
||||||
this.syncSelectionWithProject();
|
this.syncSelectionWithProject();
|
||||||
|
|
||||||
|
// Resume polling if generating
|
||||||
|
const pollingStatuses = ['queued', 'generating', 'rendering'];
|
||||||
|
if (pollingStatuses.includes(this.state.project.status)) {
|
||||||
|
this.state.currentStep = 6;
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.createProject();
|
await this.createProject();
|
||||||
@ -708,7 +715,64 @@ const App = {
|
|||||||
|
|
||||||
if (this.state.currentStep < this.state.totalSteps) {
|
if (this.state.currentStep < this.state.totalSteps) {
|
||||||
this.showStep(this.state.currentStep + 1);
|
this.showStep(this.state.currentStep + 1);
|
||||||
|
|
||||||
|
// Trigger generation if entering step 6
|
||||||
|
if (this.state.currentStep === 6) {
|
||||||
|
this.startWebsiteGeneration();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async startWebsiteGeneration() {
|
||||||
|
try {
|
||||||
|
const result = await this.apiCall('generateWebsite');
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Generation started:', result.data.task_id);
|
||||||
|
this.startPolling();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start generation:', error);
|
||||||
|
document.getElementById('generation-status').textContent = 'Chyba: ' + error.message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
if (this.pollingInterval) clearInterval(this.pollingInterval);
|
||||||
|
|
||||||
|
this.pollingInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await this.apiCall('getProjectStatus');
|
||||||
|
if (response.success) {
|
||||||
|
const project = response.data;
|
||||||
|
this.state.project = project;
|
||||||
|
this.updateGenerationStatus(project.status);
|
||||||
|
|
||||||
|
if (project.status === 'ready' || project.status === 'failed') {
|
||||||
|
clearInterval(this.pollingInterval);
|
||||||
|
if (project.status === 'ready') {
|
||||||
|
// Move to preview step
|
||||||
|
this.showStep(7);
|
||||||
|
} else {
|
||||||
|
document.getElementById('generation-status').textContent = 'Generovanie zlyhalo. Skúste to neskôr.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Polling error:', error);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateGenerationStatus(status) {
|
||||||
|
const statusEl = document.getElementById('generation-status');
|
||||||
|
const messages = {
|
||||||
|
'queued': 'Zaradené do fronty, čakám na AI...',
|
||||||
|
'generating': 'AI práve píše texty pre váš web...',
|
||||||
|
'rendering': 'Skladáme výslednú stránku...',
|
||||||
|
'ready': 'Hotovo! Pripravujem náhľad...',
|
||||||
|
'failed': 'Nastal problém pri generovaní.'
|
||||||
|
};
|
||||||
|
statusEl.textContent = messages[status] || 'Spracovávam...';
|
||||||
},
|
},
|
||||||
|
|
||||||
prevStep() {
|
prevStep() {
|
||||||
@ -749,6 +813,9 @@ const App = {
|
|||||||
|
|
||||||
btnNext.disabled = nextDisabled;
|
btnNext.disabled = nextDisabled;
|
||||||
|
|
||||||
|
// Hide footer in generating step
|
||||||
|
document.querySelector('.wizard-footer').classList.toggle('hidden', this.state.currentStep === 6);
|
||||||
|
|
||||||
if (this.state.currentStep === this.state.totalSteps) {
|
if (this.state.currentStep === this.state.totalSteps) {
|
||||||
btnNext.textContent = 'Dokončiť';
|
btnNext.textContent = 'Dokončiť';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
66
src/Actions/TaskActions.php
Normal file
66
src/Actions/TaskActions.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Actions;
|
||||||
|
|
||||||
|
use App\Services\FileStorage;
|
||||||
|
use App\Services\ConsentService;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class TaskActions
|
||||||
|
{
|
||||||
|
private FileStorage $storage;
|
||||||
|
private ProjectActions $projectActions;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->storage = new FileStorage();
|
||||||
|
$this->projectActions = new ProjectActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new AI generation task for a project.
|
||||||
|
*/
|
||||||
|
public function generateWebsite(string $userId, string $projectId): array
|
||||||
|
{
|
||||||
|
// 1. Verify project ownership and status
|
||||||
|
$projectData = $this->projectActions->getProjectStatus($userId, $projectId);
|
||||||
|
|
||||||
|
// 2. Prevent duplicate queuing
|
||||||
|
if ($projectData['status'] === 'queued' || $projectData['status'] === 'generating') {
|
||||||
|
return ['message' => 'Generation already in progress.', 'status' => $projectData['status']];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Verify GDPR consent
|
||||||
|
$consentService = new ConsentService();
|
||||||
|
if (!$consentService->hasConsent($projectId)) {
|
||||||
|
throw new Exception("GDPR consent is required for AI generation.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create Task
|
||||||
|
$taskId = 't_' . bin2hex(random_bytes(8));
|
||||||
|
$taskData = [
|
||||||
|
'task_id' => $taskId,
|
||||||
|
'project_id' => $projectId,
|
||||||
|
'status' => 'queued',
|
||||||
|
'attempt_count' => 0,
|
||||||
|
'max_attempts' => 3,
|
||||||
|
'created_at' => date('c'),
|
||||||
|
'wizard_data' => $projectData['wizard_data']
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->storage->put("llm/{$taskId}.json", $taskData);
|
||||||
|
|
||||||
|
// 5. Update Project Status
|
||||||
|
$projectData['status'] = 'queued';
|
||||||
|
$projectData['updated_at'] = date('c');
|
||||||
|
$this->storage->put("projects/{$projectId}.json", $projectData);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'task_id' => $taskId,
|
||||||
|
'status' => 'queued',
|
||||||
|
'message' => 'Project successfully queued for generation.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user