Mega file creato il 18/12/2025 alle 18:30:38,46 ================================================ Cartella di partenza: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator Inclusi solo file .php, .js, .tsx, .jsx, .json Cartelle escluse: plugins, node_modules, lib, dist, android, .expo ================================================ ----- File: base.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\base.php ---------------------------------------- 0, 'sections_failed' => 0, 'refinement_cycles' => 0, 'validation_passes' => 0, 'validation_failures' => 0, 'images_processed' => 0, ]; protected const WIZARD_ICON = FRAMEWORK_COMMON_CDN . "assets/icons/page-generation.json"; protected const POSITIVE_CORRECTION_MESSAGES = [ "{{Sto rifinendo un dettaglio per renderlo perfetto...}}", "{{Aggiungo un tocco di classe per un risultato impeccabile}}.", "{{Quasi perfetto! Sto solo lucidando questo elemento}}.", "{{Rielaboro questo punto per un impatto visivo ancora maggiore}}.", "{{Ottimizzo la composizione visiva per un effetto più professionale}}.", "{{Bilancio gli spazi e i colori per maggiore armonia}}." ]; public function __construct() { $this->cacheDir = __DIR__ . '/cache'; $this->patternsDir = __DIR__ . '/patterns'; $realClient = \MSFramework\AI\generator\Engines\factory::create($this->aiProvider, [ 'gemini' => $this->geminiApiKey, 'grok' => $this->grokApiKey ]); // Avvolgiamo il client in una classe anonima che intercetta le chiamate $this->aiClient = new class($realClient, $this) { private $client; public $tracker; public function __construct($client, $tracker) { $this->client = $client; $this->tracker = $tracker; } public function generateContent(string $sys, string $user, array $conf, array $imgs = []): object { $response = $this->client->generateContent($sys, $user, $conf, $imgs); if (isset($response->usageMetadata)) { $this->tracker->trackUsage($response->usageMetadata); } return $response; } }; if (!is_dir($this->patternsDir)) { mkdir($this->patternsDir, 0775, true); } if (!is_dir($this->cacheDir)) { mkdir($this->cacheDir, 0775, true); } } // =================================================================== // SHARED UTILITIES (Cache, Logging, Download, JSON) // =================================================================== protected function logMessage($message) { $timestamp = date('Y-m-d H:i:s'); $logFile = $this->cacheDir . '/generator_base.log'; file_put_contents($logFile, "[{$timestamp}] {$message}\n", FILE_APPEND); } protected function checkCache($jobId) { $cacheFile = $this->cacheDir . '/' . $jobId . '.json'; if (file_exists($cacheFile)) { $cacheAge = time() - filemtime($cacheFile); if ($cacheAge < 86400) { return json_decode(file_get_contents($cacheFile), true); } } return null; } protected function saveCache($jobId, $data) { $cacheFile = $this->cacheDir . '/' . $jobId . '.json'; file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT)); } protected function _downloadContent($url) { try { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_HTTPHEADER, ['ngrok-skip-browser-warning: true']); $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200 && $content) { return $content; } } catch (\Exception $e) { $this->logMessage("ERRORE download: " . $e->getMessage()); } return null; } protected function _cleanJsonResponse($text) { $text = preg_replace('/^```json\s*/i', '', $text); $text = preg_replace('/\s*```$/i', '', $text); $text = trim($text); return $text; } protected function _loadReferenceFiles() { if ($this->blocksReference !== null) { return; } $blocksFile = $this->patternsDir . '/fw360-importer-visualbuilder-blocks.json'; if (file_exists($blocksFile)) { $this->blocksReference = json_decode(file_get_contents($blocksFile), true); $this->blocksReference = $this->_preProcessJson($this->blocksReference); $this->logMessage("Caricato file blocchi: " . count($this->blocksReference['blocks_list'] ?? []) . " blocchi"); } } // =================================================================== // EXAMPLES SYSTEM (Shared) // =================================================================== public function _selectBestExamples($sectionType, $sectionPurpose) { $this->logMessage("=== SELEZIONE ESEMPI INTELLIGENTI v15 (1 LP + 5 Elements) ==="); $this->logMessage("Tipo sezione: {$sectionType}"); $this->logMessage("Scopo: {$sectionPurpose}"); $elementsContainer = \Container::get('\MSFramework\AI\Templates\elements'); $landingContainer = \Container::get('\MSFramework\AI\Templates\landingpages'); // 1. Recupera ELEMENTI SINGOLI $allElements = []; if(!in_array($sectionType, ['header', 'footer'])) { if (in_array($sectionType, ['header', 'footer'])) { $elementsTypes = [$sectionType]; } else if ($sectionType === 'hero') { $elementsTypes = ['slider']; } else { $elementsTypes = array_merge(['page', 'header', 'footer', 'slider'], array_keys($elementsContainer->getPages("generic"))); } $allElements = $elementsContainer->getElements( $elementsTypes, ['static-home-page', 'landing-page', 'sales-page', 'vsl-page'], false ); } // 2. Recupera LANDING PAGES INTERE $allLandings = $landingContainer->getDetails(); if (empty($allElements) && empty($allLandings)) { $this->logMessage("Nessun elemento o landing page disponibile per esempi"); return []; } // 3. Creazione lista candidati unificata $candidates = []; $aiSelectionList = []; // Processa Elementi if (!empty($allElements)) { foreach ($allElements as $el) { $uniqueId = 'el_' . $el['id']; $el['element_content'] = array_map(function($variant) { $variant['data'] = $this->_preProcessJson(json_decode(strtr($variant['data'], ['' => '', '' => '']), true)); return $variant; }, $el['element_content']); $candidates[$uniqueId] = [ 'id' => $uniqueId, 'title' => $el['element_title'] ?? 'Senza titolo', 'category' => $el['element_category'] ?? 'generic', 'type' => 'Single Block', 'content' => $el['element_content'] ]; $aiSelectionList[] = [ 'id' => $uniqueId, 'type' => 'Single Block', 'title' => $candidates[$uniqueId]['title'], 'category' => $candidates[$uniqueId]['category'] ]; } } // Processa Landing Pages if (!empty($allLandings)) { foreach ($allLandings as $lp) { $uniqueId = 'lp_' . $lp['id']; $elementContent = json_decode($lp['element_content'], true); $elementContent = array_map(function($variant) { $variant['data'] = $this->_preProcessJson(json_decode(strtr($variant['data'], ['' => '', '' => '']), true)); return $variant; }, $elementContent); $candidates[$uniqueId] = [ 'id' => $uniqueId, 'title' => $lp['element_title'] ?? 'Landing Page', 'category' => 'Full Page Template', 'type' => 'Full Page', 'content' => $elementContent ]; $aiSelectionList[] = [ 'id' => $uniqueId, 'type' => 'Full Page', 'title' => $candidates[$uniqueId]['title'], 'category' => 'Complete Layout' ]; } } $elementsJson = json_encode($aiSelectionList, JSON_PRETTY_PRINT); // 4. Costruzione Prompt (Logica Aggiornata per 1 LP + 5 Elements) $systemPrompt = << 0.3, 'topP' => 0.9, 'topK' => 40, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $jsonText = $this->_cleanJsonResponse($response->text()); $selection = json_decode($jsonText, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->logMessage("Errore parsing selezione esempi: " . json_last_error_msg()); return []; } $selectedExamples = []; // A. Aggiungi la Landing Page (1) if (!empty($selection['selectedLandingId']) && isset($candidates[$selection['selectedLandingId']])) { $lpId = $selection['selectedLandingId']; $selectedExamples[] = [ 'id' => $candidates[$lpId]['id'], 'title' => "(FULL PAGE REF) " . $candidates[$lpId]['title'], 'content' => $this->_preProcessJson($candidates[$lpId]['content']) ]; } // B. Aggiungi gli Elementi (5) if (!empty($selection['selectedElementIds']) && is_array($selection['selectedElementIds'])) { // Prendiamo max 5 elementi $elIds = array_slice($selection['selectedElementIds'], 0, 5); foreach ($elIds as $elId) { if (isset($candidates[$elId])) { $selectedExamples[] = [ 'id' => $candidates[$elId]['id'], 'title' => "(BLOCK REF) " . $candidates[$elId]['title'], 'content' => $this->_preProcessJson($candidates[$elId]['content']) ]; } } } $this->logMessage("Esempi recuperati: " . count($selectedExamples) . " (Target: 1 LP + 5 Blocks)"); return $selectedExamples; } catch (\Exception $e) { $this->logMessage("ERRORE durante selezione esempi: " . $e->getMessage()); return []; } } private function _buildExampleSelectionSystemPrompt() { return << $example) { $formatted .= "\n**ESEMPIO " . ($i + 1) . ": {$example['title']}**\n"; if (is_array($example['content'])) { foreach ($example['content'] as $variantKey => $variant) { if (isset($variant['data'])) { $formatted .= "Variante {$variantKey}:\n"; $formatted .= "```\n" . json_encode($variant['data']) . "\n```\n"; } } } } $formatted .= "\n--- FINE ESEMPI ---\n"; return $formatted; } // =================================================================== // PROCESSING & VALIDATION (Shared) // =================================================================== protected function _assemblePage($generatedRows) { $settingsRow = [ 'type' => 'settings', 'json' => ['css' => '', 'js' => '', 'deviceOnly' => []] ]; return ['rows' => array_merge($generatedRows, [$settingsRow])]; } protected function _validateGeneratedSection($sectionData) { $errors = []; if (!is_array($sectionData)) return ['valid' => false, 'errors' => ["Output non è un array JSON"]]; $validateRow = function($row, $pathPrefix) use (&$errors, &$validateRow) { if (($row['type'] ?? '') !== 'row') { // UPDATE Issue 3: In contesti nidificati potremmo essere indulgenti, ma la radice deve essere row $errors[] = "$pathPrefix: type deve essere 'row'. Trovato: " . ($row['type'] ?? 'null'); } if (isset($row['children']) && is_array($row['children'])) { foreach ($row['children'] as $c => $col) { $colPath = "$pathPrefix Col $c"; if (($col['type'] ?? '') !== 'col') { $errors[] = "$colPath: type deve essere 'col'."; } if (isset($col['children']) && is_array($col['children'])) { foreach ($col['children'] as $b => $child) { $childPath = "$colPath Child $b"; $childType = $child['type'] ?? ''; // UPDATE Issue 3: Supporto esplicito per Row annidate if ($childType === 'row') { $validateRow($child, $childPath); } elseif ($childType === 'block') { if (isset($child['content']) && !isset($child['json']['data'])) { $errors[] = "$childPath: Hai messo 'content' alla radice. ERRORE. I dati vanno in json.data."; } } else { $errors[] = "$childPath: type deve essere 'block' oppure 'row' (nidificata). Trovato: '$childType'."; } } } } } }; foreach ($sectionData as $r => $row) { $validateRow($row, "Row $r"); } return ['valid' => empty($errors), 'errors' => array_slice($errors, 0, 3)]; } protected function _autoCorrectSection($sectionData) { $newSectionData = []; foreach ($sectionData as $row) { if (!isset($row['type'])) $row['type'] = 'row'; if (!isset($row['children']) || !is_array($row['children'])) $row['children'] = []; // UPDATE Issue 3: Non appiattire ciecamente se la struttura è row->col->row // Manteniamo la logica di pulizia ma preserviamo le righe annidate valide // Check Colonne foreach ($row['children'] as &$col) { if (!isset($col['type'])) $col['type'] = 'col'; if (isset($col['children']) && is_array($col['children'])) { foreach ($col['children'] as &$child) { // Se è una riga annidata, assicuriamoci che abbia type row if (isset($child['children']) && !isset($child['type'])) { // Euristicamente, se ha children è una row o col $child['type'] = 'row'; } if (!isset($child['type'])) $child['type'] = 'block'; // Sync block_type if (($child['type'] === 'block') && isset($child['block_type']) && !isset($child['json']['data']['type'])) { $child['json']['data']['type'] = $child['block_type']; } } } } $newSectionData[] = $row; } return $newSectionData; } protected function _walkJsonBlocks($rows, $callback) { foreach ($rows as $row) { foreach ($row['children'] ?? [] as $col) { foreach ($col['children'] ?? [] as $block) { if (($block['type'] ?? '') === 'block') { $callback($block); } } } } } // =================================================================== // IMAGE UTILITIES // =================================================================== protected function _saveImageFromUrl(string $imageUrl): ?array { try { $filename = basename(parse_url($imageUrl, PHP_URL_PATH)); $filename = md5($imageUrl) . '-' . preg_replace('/[^A-Za-z0-9\.\-\_]/', '', $filename); if (empty($filename)) $filename = md5($imageUrl) . '.jpg'; $saveImage = (new \MSFramework\uploads("PAGEGALLERY"))->addMediaFromPath($imageUrl, $filename); if ($saveImage && !empty($saveImage['directory']) && !empty($saveImage['filename'])) { return ['directory' => $saveImage['directory'], 'filename' => $saveImage['filename']]; } } catch (\Exception $e) { $this->logMessage("ERRORE durante il salvataggio dell'immagine da URL {$imageUrl}: " . $e->getMessage()); } return null; } protected function _generateAIImage($prompt, $proportion, $background) { $cacheKey = md5('gemini' . $prompt . $proportion . $background); $jobCacheDir = $this->cacheDir . '/' . $cacheKey; $imageInfoFile = $jobCacheDir . '/image.json'; if (file_exists($imageInfoFile)) { $decoded = json_decode(file_get_contents($imageInfoFile), true); // CORREZIONE 1: Accesso diretto a $this->tracker (che è $this in questo contesto) // Se aiClient->tracker->generatedImagesCount funziona altrove, qui usa direttamente $this $this->generatedImagesCount++; if (isset($decoded['cost']['input'])) { $this->imgInputTokensUsed += (int)$decoded['cost']['input']; } if (isset($decoded['cost']['output'])) { $this->imgOutputTokensUsed += (int)$decoded['cost']['output']; } return $decoded['image'] ?: $decoded; } if (!is_dir($jobCacheDir)) { mkdir($jobCacheDir, 0775, true); } \Container::get('\MSFramework\Framework\wizard')->setUpdates("{{Genero una foto}}", $prompt, self::WIZARD_ICON); try { $imageClass = new \MSFramework\AI\openai($this->openAIKey); $options = ($background === 'transparent') ? ['background' => 'transparent'] : []; $usedTokens = ['input' => 0.0, 'output' => 0.0]; $imageURL = $imageClass->generateImage($prompt, $proportion, 'gpt-image-1.5', $options, $usedTokens); // ACCUMULO TOKEN IMMAGINI if (isset($usedTokens['input'])) { $this->imgInputTokensUsed += (int)$usedTokens['input']; } if (isset($usedTokens['output'])) { $this->imgOutputTokensUsed += (int)$usedTokens['output']; } \Container::get('\MSFramework\Framework\wizard')->setUpdateSteps('+1'); if ($imageURL) { $this->aiClient->tracker->generatedImagesCount++; $filenamePrompt = "Crea un nome file per un'immagine descritta come '{$prompt}'. Il nome deve essere breve, in minuscolo, usare trattini al posto degli spazi, e finire con .png. Rispondi solo con il nome del file."; $filename = 'generated-image.png'; try { $config = ['temperature' => 0.7, 'topP' => 0.9, 'topK' => 40, 'jsonMode' => true]; $response = $this->aiClient->generateContent('', $filenamePrompt, $config); $rawFilename = $this->_cleanJsonResponse($response->text()); if (!empty($rawFilename)) { $filename = str_replace(['_', ' '], '-', \Container::get('\MSFramework\url')->cleanString($rawFilename, false)); } } catch (\Exception $e) { $this->logMessage("ERRORE generazione nome file immagine: " . $e->getMessage()); } $saveImage = (new \MSFramework\uploads("PAGEGALLERY"))->addMediaFromPath($imageURL, $filename); if ($saveImage) { $imageData = ['directory' => $saveImage['directory'], 'filename' => $saveImage['filename']]; file_put_contents($imageInfoFile, json_encode(['image' => $imageData, 'cost' => $usedTokens], JSON_PRETTY_PRINT)); return $imageData; } } } catch (\Exception $e) { $this->logMessage("ERRORE generazione immagine: " . $e->getMessage()); } return null; } protected function _preProcessJson($json) { $utils = \Container::get('\MSFramework\utils'); // --------------------------------------------------------- // FASE 1: Rimozione chiavi inutili // --------------------------------------------------------- $json = $utils->array_map_recursive(function($item) { if (!is_array($item)) return $item; // Rimuovo le chiavi inutili $keysToDelete = ['ai-provider', 'ai-destination', 'ai-orientation', 'ai-query']; foreach($keysToDelete as $key) { if($item[$key]) unset($item[$key]); } // Rimuovo le liste inutili if($item['voices'] && count($item['voices']) > 5) { $item['voices'] = array_slice($item['voices'], 0, 5); } if($item['tabs'] && count($item['tabs']) > 5) { $item['tabs'] = array_slice($item['tabs'], 0, 5); } // Rimuovo tutte le chiavi vuote //$item = array_filter($item); return $item; }, $json, true); // --------------------------------------------------------- // FASE 2: Applicazione Placeholder // --------------------------------------------------------- return $utils->array_map_recursive(function($item) { if (!is_array($item)) return $item; // Ensure json structure exists for following checks if (!isset($item['json']) || !is_array($item['json'])) return $item; // Inserisco la struttura base dove manca if (!isset($item['json']['design'])) $item['json']['design'] = []; if (!isset($item['json']['responsive'])) $item['json']['responsive'] = []; // GESTIONE DATA (Blocchi, Plugin, AI Gallery) if(isset($item['json']['data']['type']) && is_array($item['json']['data'])) { $blockType = $item['json']['data']['type']; $blockDetails = \Container::get('\MSFramework\Frontend\visualBuilder')->getBlocksDetails($blockType); // Svuoto la struttura (Verrà generata autonomamente) if ($blockDetails['structure_info']) { $item['json']['structure'] = ['data' => []]; } // BLOCCHI SPECIFICI E PLUGINS switch ($blockType) { case 'hidden/row': if(is_string($item['json']['design'])) break; $item['json']['design']['background']['ai-query'] = '(AI: Inserire query generazione immagine sfondo | SOLO SE NECESSARIA)'; $item['json']['design']['background']['ai-proportion'] = '(AI: Inserire proporzione immagine (Es 1:1, 16:9, etc...) | SOLO SE NECESSARIA)'; unset( $item['json']['design']['background']['ai-query'], $item['json']['design']['background']['ai-proportion'], $item['json']['design']['background']['ai-background'], $item['json']['design']['background']['ai-orientation'], $item['json']['design']['background']['ai-provider'] ); break; case 'main/menu': $item['json']['data']['id'] = 0; $item['json']['data']['source_mode'] = 'manual'; $item['json']['data']['manual_items'] = [ 'voice1' => ['title' => 'Voce 1', 'target' => '_self', 'url' => '#link-1'], 'voice2' => ['title' => 'Voce 2', 'target' => '_self', 'url' => '#link-2'], ]; break; case 'main/html-editor': $item['json']['data']['content'] = '(Inserisci qui HTML)'; break; case 'main/iframe': $item['json']['data']['url'] = '(Inserisci qui URL)'; break; case 'main/single-image': $item['json']['data']['image']['ai-query'] = '(AI: Inserire query generazione immagine)'; $item['json']['data']['image']['ai-proportion'] = '(AI: Inserire proporzione immagine (Es 1:1, 16:9, etc...))'; break; case 'main/gallery': $item['json']['data']['ai-gallery'] = [ 'ai-proportion' => '(AI: Inserire proporzione immagini (Es 1:1, 16:9, etc...))', 'ai-query' => [ '(AI: Inserire query generazione immagine 1)', '(AI: Inserire query generazione immagine 2)', '(AI: Inserire query generazione immagine 3)', '(AI: Inserire query generazione immagine 4)', '(AI: Inserire query generazione immagine 5)', '(AI: Inserire query generazione immagine 6)', '...', ] ]; break; } } return $item; }, $json, true); } public function _postProcessJson($json, $mode, $customMap = null) { $utils = \Container::get('\MSFramework\utils'); // --------------------------------------------------------- // FASE 1: PREPARAZIONE (Fix sui dati grezzi alla radice) // --------------------------------------------------------- $jsonPhase1 = $utils->array_map_recursive(function($item) { if (!is_array($item)) return $item; // 1. NORMALIZZAZIONE STRUTTURA BASE if (isset($item['type']) && in_array($item['type'], ['row', 'col', 'block'])) { if (!isset($item['json']) || !is_array($item['json'])) $item['json'] = []; if (!isset($item['json']['data']) || !is_array($item['json']['data'])) $item['json']['data'] = []; if($item['type'] === 'row') $item['json']['data']['type'] = 'hidden/row'; if($item['type'] === 'col') $item['json']['data']['type'] = 'hidden/col'; } // ------------------------------------------------------------- // FIX OFFSET -> SPACER COLUMN // ------------------------------------------------------------- if (($item['type'] ?? '') === 'row' && !empty($item['children']) && is_array($item['children'])) { $newChildren = []; $hasChanges = false; foreach ($item['children'] as $col) { // Controlliamo se la colonna ha un offset Desktop definito $desktopOffset = intval($col['json']['responsive']['desktop']['offset'] ?? ($col['offset'] ?? 0)); if ($desktopOffset > 0) { $hasChanges = true; // 1. Creiamo la colonna Spacer (Vuota) $spacerCol = [ 'type' => 'col', 'col' => $desktopOffset, 'children' => [], // Vuota 'json' => [ 'data' => ['type' => 'hidden/col'], 'responsive' => [ 'desktop' => ['col' => $desktopOffset], // NASCOSTA SU TABLET E MOBILE 'tablet' => ['col' => 0, 'hide' => '1'], 'mobile' => ['col' => 0, 'hide' => '1'] ], 'design' => [] ] ]; // 2. Puliamo l'offset dalla colonna originale if (isset($col['json']['responsive']['desktop']['offset'])) { unset($col['json']['responsive']['desktop']['offset']); } if (isset($col['offset'])) { unset($col['offset']); } // 3. Inseriamo: Spacer -> Colonna Originale $newChildren[] = $spacerCol; $newChildren[] = $col; } else { // Nessun offset, manteniamo la colonna così com'è $newChildren[] = $col; } } if ($hasChanges) { $item['children'] = $newChildren; } } // 14. FIX TIPI ARRAY VUOTI if(isset($item['design'])) { if (!is_array($item['design'])) $item['design'] = []; if (!empty($item['design']) && is_numeric(array_key_first($item['design']))) { $item['design'] = $item['design'][0]; } } // Applico lo sfondo cover $keyToMove = ['size', 'position']; foreach($keyToMove as $key) { if($item['json']['design']['background']['image'][$key]) { $item['json']['design']['background'][$key] = $item['json']['design']['background']['image'][$key]; unset($item['json']['design']['background']['image'][$key]); } } if(isset($item['display']) && !is_array($item['display'])) { $item['display'] = []; } if(isset($item['json']['responsive']) && ($item['type'] ?? '') !== 'col') { $item['json']['responsive'] = null; } if(isset($item['ai-border'])) { unset($item['ai-border']); $item['top-left'] = 12; $item['top-right'] = 12; $item['bottom-left'] = 12; $item['bottom-right'] = 12; $item['suffix'] = 'px'; } // 5. LEGACY FIXES if (isset($item['font-family']) && is_string($item['font-family']) && (str_contains($item['font-family'], ',') || str_contains($item['font-family'], '('))) { $item['font-family'] = trim(explode('(', explode(',', $item['font-family'])[0])[0]); } if (isset($item['line-height']) && is_array($item['line-height']) && ($item['line-height']['unit'] ?? '') !== 'em' && ($item['line-height']['value'] ?? 2) < 2) { $item['line-height']['unit'] = 'em'; } if (isset($item['z-index']) && $item['z-index'] < 0) { $item['z-index'] = ''; } if (isset($item['max-width']) && $item['max-width'] > 0) { $item['max-width'] = ''; } if(isset($item['content']) && is_array($item['content']) && isset($item['content']['gradient'])) { $item['content']['color'] = $item['content']['gradient']; unset($item['content']['gradient']); } if(isset($item['opacity']) && $item['opacity'] > 0.0 && $item['opacity'] < 1.0) { $item['opacity'] = round($item['opacity'] * 100, 2); } if(($this->stepsData['colors']['value'] ?? null) && $this->stepsData['colors']['value'] !== 'default') { $palette = $this->sectionContext['visualIdentity']['colorPalette'] ?? []; if(isset($item['color']) && array_key_exists($item['color'], $palette)) { $item['color'] = $palette[$item['color']]; } } if(isset($item['font-family']) && $item['font-family']) { $fontsList = json_decode(file_get_contents(__DIR__ . '/../../../../backend/vendor/googlefont-picker/json/google-fonts.json'), true); $realFont = ''; $maxSimilarity = 0; foreach($fontsList['items'] as $font) { if(similar_text($font['family'], $item['font-family'], $percent) && $percent > $maxSimilarity) { $maxSimilarity = $percent; $realFont = $font['family']; } } $item['font-family'] = $realFont; } if ($item['is_common'] ?? false) { $item['is_common'] = 0; } return $item; }, $json, true); // FASE 5: FIX AGGRESSIVO COLONNE if (isset($jsonPhase1[0]['type']) && $jsonPhase1[0]['type'] === 'row') { $jsonPhase1 = $this->_fixColumnWidths($jsonPhase1); } else if (isset($jsonPhase1['rows'])) { $jsonPhase1['rows'] = $this->_fixColumnWidths($jsonPhase1['rows']); } // --------------------------------------------------------- // FASE 2: MIGRAZIONE STRUTTURALE (Root -> json) // --------------------------------------------------------- $jsonPhase2 = $utils->array_map_recursive(function($item) { if (!is_array($item)) return $item; // Rileva: Una ROW che contiene UNA SOLA COLONNA, la quale contiene MOLTE IMMAGINI if (($item['type'] ?? '') === 'row' && isset($item['children']) && count($item['children']) === 1) { $singleCol = $item['children'][0]; $grandChildren = $singleCol['children'] ?? []; $count = count($grandChildren); // Se è una colonna Full Width (12) e ha più di 1 elemento if (($singleCol['col'] ?? 12) == 12 && $count > 1) { // Controlliamo se sono TUTTE immagini (tipico dei logo wall) $isLogoWall = true; foreach($grandChildren as $block) { $bType = $block['block_type'] ?? ($block['json']['data']['type'] ?? ''); if (!str_contains($bType, 'image')) { $isLogoWall = false; break; } } if ($isLogoWall) { $newColumns = []; $colSize = 3; // Default (4 per riga) if ($count % 3 == 0) $colSize = 4; // 3 per riga if ($count >= 6) $colSize = 2; // 6 per riga (per loghi piccoli) foreach($grandChildren as $imgBlock) { // Forziamo lo stile dell'immagine per essere centrata e contenuta if(!isset($imgBlock['json']['design'])) $imgBlock['json']['design'] = []; // Centriamo l'immagine nella sua nuova colonna $imgBlock['json']['design']['container']['alignment'] = 'center'; $newColumns[] = [ 'type' => 'col', 'col' => $colSize, // Desktop size calcolata 'children' => [$imgBlock], 'json' => [ 'responsive' => [ 'tablet' => ['col' => 4], // 3 per riga su tablet 'mobile' => ['col' => 6] // 2 per riga su mobile ] ] ]; } // Sostituiamo la colonna singola "monolitica" con le nuove colonne agili $item['children'] = $newColumns; // Aggiungiamo un gap verticale/orizzontale alla row per sicurezza if(!isset($item['json']['design'])) $item['json']['design'] = []; $item['json']['design']['spacing']['row-gap'] = '30px'; } } } // --------------------------------------------------- // FIX: FLATTEN NESTED COLUMNS (Col -> Col => Row -> Col, Col) // --------------------------------------------------- if (($item['type'] ?? '') === 'row' && isset($item['children']) && is_array($item['children'])) { $newChildren = []; $hasChanges = false; foreach ($item['children'] as $child) { // Verifichiamo se il figlio è una COLONNA "Wrapper" errata $isWrapperCol = false; if (($child['type'] ?? '') === 'col' && !empty($child['children']) && is_array($child['children'])) { // Controlliamo se il primo figlio è a sua volta una COL // (L'AI tende a essere coerente: se ne mette una, le mette tutte) $firstChild = reset($child['children']); if (is_array($firstChild) && ($firstChild['type'] ?? '') === 'col') { $isWrapperCol = true; } } if ($isWrapperCol) { // SCOMPATTA: Invece di aggiungere la colonna wrapper, aggiungiamo i suoi figli foreach ($child['children'] as $innerChild) { $newChildren[] = $innerChild; } $hasChanges = true; } else { // Mantiene il figlio originale $newChildren[] = $child; } } if ($hasChanges) { $item['children'] = $newChildren; } } // Gestione movimento dati dentro 'json' if((isset($item['json']) && is_array($item['json'])) || !empty($item['children'])) { if(!isset($item['json']) || !is_array($item['json'])) $item['json'] = []; $dataToMove = []; foreach (['design', 'responsive', 'display', 'overlay', 'onclick', 'animation', 'ai-conditions'] as $key) { if (isset($item[$key]) && is_array($item[$key]) && !empty($item[$key])) { $dataToMove[$key] = $item[$key]; unset($item[$key]); // Rimuoviamo dalla root } } foreach ($dataToMove as $key => $data) { $item['json'][$key] = array_replace_recursive($item['json'][$key] ?? [], $data); } } // 3. SYNC BLOCK TYPE & DATA if (isset($item['block_type'])) { if(empty($item['type'])) $item['type'] = 'block'; if (!isset($item['json']['data'])) $item['json']['data'] = []; $item['json']['data']['type'] = $item['block_type']; } // 4. Fix comuni campi errati if(isset($item['json']['data']['type']) && is_array($item['json']['data'])) { $blockType = $item['json']['data']['type']; switch ($blockType) { case 'main/single-image': if($item['json']['data']['ai-query']) { if(!isset($item['json']['data']['image']) || !is_array($item['json']['data']['image'])) { $item['json']['data']['image'] = []; } foreach(['ai-query', 'ai-proportion', 'ai-background', 'ai-orientation', 'ai-provider'] as $aiKey) { if(!$item['json']['data'][$aiKey]) continue; $item['json']['data']['image'][$aiKey] = $item['json']['data'][$aiKey]; unset($item['json']['data'][$aiKey]); } } break; case 'main/gallery': if($item['json']['data']['ai-gallery']) { if($item['json']['data']['ai-gallery']['query']) { $item['json']['data']['ai-gallery']['ai-query'] = $item['json']['data']['ai-gallery']['query']; if(!$item['json']['data']['ai-gallery']['ai-proportion']) $item['json']['data']['ai-gallery']['ai-proportion'] = '1:1'; unset($item['json']['data']['ai-gallery']['query']); } } break; case 'hidden/row': if($item['json']['design']['background'] && $item['json']['design']['background']['image']['ai-query']) { $item['json']['design']['background']['ai-query'] = $item['json']['design']['background']['image']['ai-query']; $item['json']['design']['background']['ai-proportion'] = $item['json']['design']['background']['image']['ai-proportion']; $item['json']['design']['background']['ai-background'] = $item['json']['design']['background']['image']['ai-background']; unset($item['json']['design']['background']['image']); } break; } } // ========================================================= // FIX RESPONSIVE & LAYOUT MOBILE (Header & Padding) // ========================================================= // 1. RESET GLOBALE PADDING LATERALE SU MOBILE if (isset($item['json']['design']['mobile']['padding'])) { // Impostiamo un padding minimo di sicurezza (es. 10px o 0px) // Se vuoi 0 assoluto metti '0', ma '10' è spesso meglio per evitare che il testo tocchi i bordi dello schermo $item['json']['design']['mobile']['padding']['left'] = '10'; $item['json']['design']['mobile']['padding']['right'] = '10'; } // Rimuoviamo anche i margini negativi o eccessivi se presenti if (isset($item['json']['design']['mobile']['margin'])) { $item['json']['design']['mobile']['margin']['left'] = '0'; $item['json']['design']['mobile']['margin']['right'] = '0'; } // Unifica le colonne con delle icone affiancate if (($item['type'] ?? '') === 'row' && isset($item['children']) && count($item['children']) > 1) { $isIconRow = true; $collectedIcons = []; // 1. Analisi: Verifichiamo se TUTTE le colonne contengono SOLO icone foreach ($item['children'] as $col) { // Deve essere una colonna if (($col['type'] ?? '') !== 'col') { $isIconRow = false; break; } // Deve avere contenuto if (empty($col['children'])) { $isIconRow = false; break; } // Controlliamo i blocchi interni foreach ($col['children'] as $block) { $bType = $block['block_type'] ?? ($block['json']['data']['type'] ?? ''); // Se troviamo qualcosa che NON è un'icona (es. testo), annulliamo l'operazione // Vogliamo unire solo le stelline o gruppi di icone pure if ($bType !== 'main/icon') { $isIconRow = false; break 2; // Esce da entrambi i loop } $collectedIcons[] = $block; } } // 2. Trasformazione: Se è una riga di sole icone, unifichiamo if ($isIconRow && count($collectedIcons) > 1) { // Configuriamo la nuova colonna unica $newSingleCol = [ 'type' => 'col', 'col' => 12, 'children' => $collectedIcons, 'json' => [ 'data' => [ 'type' => 'hidden/col' // Mantiene la tipologia standard ], 'responsive' => [ 'desktop' => ['col' => 12] ], 'design' => [ 'content' => [ 'align' => 'center', // Centra orizzontalmente 'vertical-align' => 'center', // Centra verticalmente 'flex-direction' => 'initial' // <--- LA CHIAVE MAGICA: Affianca gli elementi ] ] ] ]; // Sostituiamo i figli della riga con la nuova colonna unica $item['children'] = [$newSingleCol]; // Opzionale: Rimuoviamo il gap della riga poiché ora gestiamo tutto dentro la colonna if (!isset($item['json']['design']['content'])) $item['json']['design']['content'] = []; $item['json']['design']['content']['col-spacing'] = '0'; } } // Sistema l'allineamento di eventuali più pulsanti allineati orizzontalmente if (($item['type'] ?? '') === 'row' && isset($item['children']) && count($item['children']) === 2) { $col1 = $item['children'][0] ?? null; $col2 = $item['children'][1] ?? null; // Controlliamo se sono colonne valide if (($col1['type'] ?? '') === 'col' && ($col2['type'] ?? '') === 'col') { // Controlliamo il contenuto (deve essere button) // Usiamo isset/empty per evitare warning su strutture incomplete $bType1 = $col1['children'][0]['block_type'] ?? ($col1['children'][0]['json']['data']['type'] ?? ''); $bType2 = $col2['children'][0]['block_type'] ?? ($col2['children'][0]['json']['data']['type'] ?? ''); if ($bType1 === 'main/button' && $bType2 === 'main/button') { // --- COLONNA 1 (Sinistra) --- // Desktop: 6 col, no offset, align right (verso il centro) $item['children'][0]['col'] = 6; if(!isset($item['children'][0]['json']['responsive']['desktop'])) $item['children'][0]['json']['responsive']['desktop'] = []; $item['children'][0]['json']['responsive']['desktop']['col'] = 6; unset($item['children'][0]['json']['responsive']['desktop']['offset']); // RIMUOVIAMO L'OFFSET unset($item['children'][0]['offset']); // RIMUOVIAMO L'OFFSET RADICE if(!isset($item['children'][0]['json']['design']['content'])) $item['children'][0]['json']['design']['content'] = []; $item['children'][0]['json']['design']['content']['align'] = 'flex-end'; // Tablet Override: Forza 6 col + Align Right (o Center se preferisci impilarli) if(!isset($item['children'][0]['json']['responsive']['tablet'])) $item['children'][0]['json']['responsive']['tablet'] = []; $item['children'][0]['json']['responsive']['tablet']['col'] = 6; unset($item['children'][0]['json']['responsive']['tablet']['offset']); if(!isset($item['children'][0]['json']['design']['tablet']['content'])) $item['children'][0]['json']['design']['tablet']['content'] = []; $item['children'][0]['json']['design']['tablet']['content']['align'] = 'flex-end'; // Mobile Override: Forza 12 col + Center if(!isset($item['children'][0]['json']['responsive']['mobile'])) $item['children'][0]['json']['responsive']['mobile'] = []; $item['children'][0]['json']['responsive']['mobile']['col'] = 12; unset($item['children'][0]['json']['responsive']['mobile']['offset']); if(!isset($item['children'][0]['json']['design']['mobile']['content'])) $item['children'][0]['json']['design']['mobile']['content'] = []; $item['children'][0]['json']['design']['mobile']['content']['align'] = 'center'; // Margine inferiore su mobile per separare i bottoni if(!isset($item['children'][0]['json']['responsive']['mobile']['margin'])) $item['children'][0]['json']['responsive']['mobile']['margin'] = []; $item['children'][0]['json']['responsive']['mobile']['margin']['bottom'] = '15'; $item['children'][0]['json']['responsive']['mobile']['margin']['suffix'] = 'px'; // --- COLONNA 2 (Destra) --- // Desktop: 6 col, no offset, align left (verso il centro) $item['children'][1]['col'] = 6; if(!isset($item['children'][1]['json']['responsive']['desktop'])) $item['children'][1]['json']['responsive']['desktop'] = []; $item['children'][1]['json']['responsive']['desktop']['col'] = 6; unset($item['children'][1]['json']['responsive']['desktop']['offset']); unset($item['children'][1]['offset']); if(!isset($item['children'][1]['json']['design']['content'])) $item['children'][1]['json']['design']['content'] = []; $item['children'][1]['json']['design']['content']['align'] = 'flex-start'; // Tablet Override: Forza 6 col + Align Left if(!isset($item['children'][1]['json']['responsive']['tablet'])) $item['children'][1]['json']['responsive']['tablet'] = []; $item['children'][1]['json']['responsive']['tablet']['col'] = 6; unset($item['children'][1]['json']['responsive']['tablet']['offset']); if(!isset($item['children'][1]['json']['design']['tablet']['content'])) $item['children'][1]['json']['design']['tablet']['content'] = []; $item['children'][1]['json']['design']['tablet']['content']['align'] = 'flex-start'; // Mobile Override: Forza 12 col + Center if(!isset($item['children'][1]['json']['responsive']['mobile'])) $item['children'][1]['json']['responsive']['mobile'] = []; $item['children'][1]['json']['responsive']['mobile']['col'] = 12; unset($item['children'][1]['json']['responsive']['mobile']['offset']); if(!isset($item['children'][1]['json']['design']['mobile']['content'])) $item['children'][1]['json']['design']['mobile']['content'] = []; $item['children'][1]['json']['design']['mobile']['content']['align'] = 'center'; // Reset margin top che a volte l'AI mette sul secondo elemento if(isset($item['children'][1]['json']['responsive']['mobile']['margin']['top'])) { $item['children'][1]['json']['responsive']['mobile']['margin']['top'] = '0'; } } } } return $item; }, $jsonPhase1, true); // --------------------------------------------------------- // FASE 3: LOGICA APPLICATIVA (Eseguita su struttura standardizzata) // --------------------------------------------------------- $jsonPhase3 = $utils->array_map_recursive(function($item) use ($mode, $customMap) { if (!is_array($item)) return $item; // Ensure json structure exists for following checks if (!isset($item['json']) || !is_array($item['json'])) return $item; // 4. SANITIZZAZIONE DESIGN (COLORI & OVERLAY) if (isset($item['json']['design']) && is_array($item['json']['design'])) { $this->_convertTransparentBackgroundToOverlay($item['json']['design']); $this->_sanitizeDesignColors($item['json']['design']); } // Anche nel responsive if (isset($item['json']['responsive']) && is_array($item['json']['responsive'])) { foreach($item['json']['responsive'] as &$deviceConfig) { if(isset($deviceConfig['design'])) { $this->_convertTransparentBackgroundToOverlay($deviceConfig['design']); $this->_sanitizeDesignColors($deviceConfig['design']); } } } // Aggiungo i dati mancanti nelle strutture complesse if (isset($item['json']['data']['type'])) { $bType = $item['json']['data']['type']; if (in_array($bType, ['main/carousel', 'main/tabs', 'main/accordion'])) { $item = $this->_fixComplexBlockStructures($item); } } // Rimuovo l'offset nel responsive in caso di larghezza if (isset($item['col']) && ($item['type'] ?? '') === 'col') { $currentResp = $item['json']['responsive'] ?? []; // 1. Assicuriamo che il valore desktop sia sincronizzato $desktopCol = intval($item['json']['responsive']['desktop']['col'] ?? $item['col']); $item['json']['responsive'] = array_replace_recursive($currentResp, ['desktop' => ['col' => $desktopCol]]); // 2. FIX TABLET/MOBILE FULL WIDTH if ($desktopCol === 12) { if (!isset($item['json']['responsive']['tablet'])) $item['json']['responsive']['tablet'] = []; $item['json']['responsive']['tablet']['col'] = 12; unset($item['json']['responsive']['tablet']['offset']); if (!isset($item['json']['responsive']['mobile'])) $item['json']['responsive']['mobile'] = []; $item['json']['responsive']['mobile']['col'] = 12; unset($item['json']['responsive']['mobile']['offset']); } } // 7, 8, 11. GESTIONE DATA (Blocchi, Plugin, AI Gallery) if(isset($item['json']['data']['type']) && is_array($item['json']['data'])) { $blockType = $item['json']['data']['type']; $blockDetails = \Container::get('\MSFramework\Frontend\visualBuilder')->getBlocksDetails($blockType); // 7. GESTIONE STRUTTURA PLUGINS if ($blockDetails['structure_info'] && ($s = \Container::get('\MSFramework\AI\Templates\elements')->getElements('structure', [$blockType], false))) { $s = $s[array_rand(array_keys($s))]; if (!empty($s['element_content'])) { $rawStructureJson = json_decode(strtr(array_values($s['element_content'])[0]['data'], ['' => '', '' => '']), true); // 2. Logica di Rigenerazione AI (Solo in mode 'generate') if (($mode === 'generate' || $mode === 'edit') && isset($this->aiClient)) { $visualContext = $this->sectionContext['visualIdentity'] ?? []; // Chiamata al nuovo metodo di restyling $restyledJson = $this->_regeneratePluginStructure( $rawStructureJson, $visualContext, $blockType ); // Se l'AI ha restituito qualcosa di valido, usiamo quello, altrimenti fallback sull'originale if (!empty($restyledJson)) { $rawStructureJson = $restyledJson; } } // 3. Post-process standard (ricorsivo) e assegnazione $item['json']['structure'] = ['data' => json_encode($this->_postProcessJson($rawStructureJson, $mode))]; } } // 8. BLOCCHI SPECIFICI E PLUGINS if (isset($item['json']['data']['type'])) { if (($blockDetails['plugin_id'] ?? null) && !\Container::get('\MSFramework\Appearance\plugins')->isActive($blockDetails['plugin_id'])) { \Container::get('\MSFramework\AI\Templates\elements')->installPlugin($blockDetails['plugin_id']); } if (!($item['json']['data']['theme'] ?? null) && isset($blockDetails['path']) && file_exists($blockDetails['path'] . '/styles/')) { if(file_exists($blockDetails['path'] . '/styles/default/')) { $item['json']['data']['theme'] = 'default'; } else { foreach(glob($blockDetails['path'] . '/styles/*') as $directory) { if(!$item['json']['data'][basename($directory)]) $item['json']['data'][basename($directory)] = []; $item['json']['data'][basename($directory)]['theme'] = 'default'; } } } // Aggiungo il tema ai caroselli if($item['json']['data']['main']['view-mode']['mode'] === 'carousel') { $item['json']['data']['main']['view-mode']['carousel']['arrows']['theme'] = 'default'; $item['json']['data']['main']['view-mode']['carousel']['arrows']['dots'] = 'default'; } // Se c'é uno sfondo mi assicuro che ci sia anche un padding if(!in_array($blockType, ['hidden/row', 'hidden/col']) && $item['json']['design'] && $item['json']['design']['background']['color'] && !$item['json']['design']['padding']['left']) { $item['json']['design']['padding'] = ['left' => 30, 'right' => 30, 'top' => 30, 'bottom' => 30]; } switch ($blockType) { case 'hidden/col': if($item['json']['design']['content']['align']) { $item['json']['design']['content']['align'] = ''; } // Sistemo l'allineamento degli elementi $inheritAligmentBlocks = ['main/separator', 'main/icon']; foreach($inheritAligmentBlocks as $blockID) { $unalignedTarget = array_filter($item['children'] ?: [], function($r) use($blockID) { return $r['json']['data']['type'] === $blockID && !$r['json']['design']['content']['align']; }); $alignedOthers = array_filter($item['children'] ?: [], function($r) use($blockID) { return $r['json']['data']['type'] !== $blockID && $r['json']['design']['content']['align']; }); if($unalignedTarget && $alignedOthers) { foreach($item['children'] as &$child) { if($child['json']['data']['type'] === $blockID) { $child['json']['design']['content']['align'] = array_values($alignedOthers)[0]['json']['design']['content']['align']; } } } } break; case 'plugins/general/faq/visualbuilder-blocks/main/faq': case 'main/sitemap': $item['json']['data']['id'] = 1; $item['json']['data']['theme'] = 'default'; if(!isset($item['json']['data']['navbar'])) $item['json']['data']['navbar'] = []; $item['json']['data']['navbar']['theme'] = 'default'; break; case 'main/form': $item['json']['data']['form'] = 1; break; case 'main/button': if (!isset($item['json']['data']['main']['button'])) { $item['json']['data']['main']['button'] = []; } // FIX 1: Spostamento Design (da json['design'] a button specifico) if ($item['json']['design'] && !$item['json']['data']['main']['button']['design']) { $item['json']['data']['main']['button']['design'] = $item['json']['design']; $item['json']['design'] = []; } // FIX 2: Normalizzazione Dati $fieldsToNormalize = ['text', 'icon', 'alignment']; foreach ($fieldsToNormalize as $field) { if (!empty($item['json']['data'][$field])) { $item['json']['data']['main']['button'][$field] = $item['json']['data'][$field]; unset($item['json']['data'][$field]); } } // FIX 3: Default Theme & Type if (empty($item['json']['data']['main']['button']['theme'])) { $item['json']['data']['main']['button']['theme'] = 'default'; } if (empty($item['json']['data']['main']['button']['type'])) { $item['json']['data']['main']['button']['type'] = 'primary'; } // Sanitize nested button design colors if (isset($item['json']['data']['main']['button']['design'])) { $this->_sanitizeDesignColors($item['json']['data']['main']['button']['design']); // Sistemo lo sfondo $currentBg = $item['json']['data']['main']['button']['design']['background']['color'] ?? null; if (empty($currentBg) && $currentBg !== 'transparent') { $btnType = $item['json']['data']['main']['button']['type']; $paletteColor = $this->sectionContext['visualIdentity']['colorPalette'][$btnType] ?? ''; if ($paletteColor) { $item['json']['data']['main']['button']['design']['background']['color'] = $paletteColor; } } // Rimuovo i colori del testo nei pulsanti if($currentBg !== 'transparent' && $item['json']['data']['main']['button']['design']['content'] && $item['json']['data']['main']['button']['design']['content']['color']) { $item['json']['data']['main']['button']['design']['content']['color'] = ''; } // Se è trasparente e c'è un bordo, forziamo il testo del colore del bordo (Ghost Button Logic) if ($currentBg === 'transparent' && !empty($item['json']['data']['main']['button']['design']['border']['color'])) { $item['json']['data']['main']['button']['design']['content']['color'] = $item['json']['data']['main']['button']['design']['border']['color']; } } $item['json']['data']['main']['button']['width'] = ''; break; case 'main/single-image': if(is_string($item['json']['data']['image']['files']) && str_contains($item['json']['data']['image']['files'], '.mp4')) { $item['json']['design']['image']['width'] = '100%'; } break; case 'main/carousel': // Logica esistente per la pulizia dei dati if (isset($item['json']['data']['tabs']) && is_array($item['json']['data']['tabs'])) { if (($mode === 'generate' || $mode === 'edit') && isset($this->aiClient)) { $visualContext = $this->sectionContext['visualIdentity'] ?? []; // Chiamiamo il metodo dedicato (definito nel Passo 2) $item['json']['data']['tabs'] = $this->_regenerateCarouselSlides( $item['json']['data']['tabs'], $visualContext, $mode ); } // Fallback/Pulizia standard per casi non gestiti dall'AI (es. importer) foreach ($item['json']['data']['tabs'] as &$tab) { if (isset($tab['data']) && is_string($tab['data'])) { if (!json_decode($tab['data'])) { $tab['data'] = $this->_repairMalformedJsonContent($tab['data']); } } } } break; case 'main/tabs': case 'main/accordion': // Chiave diversa per Accordion (spesso 'items') e Tabs ('tabs') $dataKey = ($blockType === 'main/accordion') ? 'items' : 'tabs'; if (isset($item['json']['data'][$dataKey]) && is_array($item['json']['data'][$dataKey])) { // ESEGUIAMO SOLO IN MODALITÀ GENERATE (come per il carosello) if (($mode === 'generate' || $mode === 'edit') && isset($this->aiClient)) { $visualContext = $this->sectionContext['visualIdentity'] ?? []; // Chiamata all'AI per rigenerare il contenuto interno $item['json']['data'][$dataKey] = $this->_regenerateTabContent( $item['json']['data'][$dataKey], $visualContext, $mode ); } else { // Fallback / Pulizia standard per Import o se AI fallisce foreach ($item['json']['data'][$dataKey] as &$tab) { if (isset($tab['data']) && is_string($tab['data'])) { if (!json_decode($tab['data'])) { $tab['data'] = $this->_repairMalformedJsonContent($tab['data']); } } } } } break; case 'main/countdown': $item['json']['data']['color'] = 'dark'; $item['json']['data']['theme'] = 'calendar'; $item['json']['data']['value'] = date('d/m/Y H:i', strtotime('+ 1 year')); // Dimezzo le font-size, spesso troppo grandi if($item['json']['design']['content']['size']['value'] && (int)$item['json']['design']['content']['size']['value'] > 20) { $item['json']['design']['content']['size']['value'] /= 2; if($item['json']['design']['mobile']['content']['size']['value'] && (int)$item['json']['design']['mobile']['content']['size']['value'] > 20) { $item['json']['design']['mobile']['content']['size']['value'] /= 2; } } // Rimuoviamo gli sfondi $item['json']['design']['background']['color'] = ''; break; case 'plugins/general/reviews/visualbuilder-blocks/main/reviews': // Rimuoviamo gli sfondi $item['json']['design']['background']['color'] = ''; $item['json']['design']['box-shadow'] = []; $item['json']['design']['border-radius'] = []; $item['json']['design']['padding'] = []; break; case 'main/counter': $item['json']['data']['transition'] = ['instant' => ['value' => 10]]; if(!$item['json']['data']['from']) $item['json']['data']['from'] = 0; if(!$item['json']['data']['to']) $item['json']['data']['to'] = 30; break; case 'main/social': $item['json']['data']['socials'] = []; foreach(\Container::get('\MSFramework\pages')->getCommonShortcodes() as $shortcodeKey => $shortcodeValue) { if(!str_contains($shortcodeKey, 'ms-social')) continue; $item['json']['data']['socials']['{' . $shortcodeKey . '}'] = '{{' . $shortcodeKey . '}}'; } break; case 'main/title-with-icon': if(!in_array($item['json']['data']['icon']['align'], ['left', 'right'])) { $item['json']['data']['icon']['align'] = 'left'; } break; case 'main/gallery': if($mode === 'generate' && $item['json']['data']['ai-gallery'] && $item['json']['data']['ai-gallery']['ai-query']) { $images = []; if(!is_array($item['json']['data']['ai-gallery']['ai-query'])) { $item['json']['data']['ai-gallery']['ai-query'] = [$item['json']['data']['ai-gallery']['ai-query']]; } $baseQuery = $item['json']['data']['ai-gallery']['ai-query']; for($i = 1; $i <= ($item['json']['data']['limit'] ?: 5); $i++) { $item['json']['data']['ai-gallery']['ai-query'] = $baseQuery[$i] ?: ($baseQuery[0] . ' (Foto ' . $i . ' di ' . ($item['json']['data']['limit'] ?: 5) . ')'); if ($img = $this->_generateImageFromData($item['json']['data']['ai-gallery'])) { $images = array_merge($images, json_decode($img, true)); } $item['json']['data']['ai-gallery'] = []; } $item['json']['data']['images'] = $images; } else { $item['json']['data']['id'] = 1; } break; case 'main/ordered-list': $item['json']['data']['icon']['typography']['design']['margin'] = ['right' => 15]; break; case 'main/menu': $item['json']['design']['hover'] = []; if($item['json']['design']['content']['color']) { $item['json']['data']['navbar']['voice']['text'] = $item['json']['design']['content']['color']; } break; case 'main/google-maps': $item['json']['data']['min-height'] = 450; $item['json']['data']['min-height-prefix'] = 'px'; break; case 'main/whatsapp': $item['json']['design']['content']['color'] = ''; break; } } } // 10. AI IMAGE GENERATION (Generico) if (($mode === 'generate' || $mode === 'edit') && isset($item['json']['data']['image']['ai-query'])) { if ($img = $this->_generateImageFromData($item['json']['data']['image'])) { $item['json']['data']['image'] = ['files' => $img]; } } else if (($mode === 'generate' || $mode === 'edit') && isset($item['json']['design']['background']['ai-query'])) { if ($img = $this->_generateImageFromData($item['json']['design']['background'])) { $item['json']['design']['background']['image'] = ['files' => $img]; $item['json']['design']['background']['size'] = 'cover'; // PULIZIA CHIAVI AI (Rimuovi anche le varianti allucinate) unset( $item['json']['design']['background']['ai-query'], $item['json']['design']['background']['ai-proportion'], $item['json']['design']['background']['ai-background'], $item['json']['design']['background']['ai-orientation'], $item['json']['design']['background']['ai-provider'] ); if($item['json']['design']['background']['overlay'] && $item['json']['design']['background']['overlay']['color']) { $item['json']['design']['overlay'] = ['color' => $item['json']['design']['background']['overlay']['color'], 'opacity' => 45]; } if($item['json']['design']['overlay']['opacity'] && $item['json']['design']['overlay'] >= 100) { $item['json']['design']['overlay']['opacity'] = 45; } } } if($customMap && is_callable($customMap)) { $item = $customMap($item); } // Final check JSON node empty structures if (!isset($item['json']['design'])) $item['json']['design'] = []; if (!isset($item['json']['responsive'])) $item['json']['responsive'] = []; return $item; }, $jsonPhase2, true); // --------------------------------------------------------- // FASE 4: CLEANUP & VALIDAZIONE // --------------------------------------------------------- return $utils->array_map_recursive(function($item) { if (!is_array($item)) return $item; // Rimuove blocchi non validi if (isset($item['children']) && is_array($item['children'])) { $item['children'] = array_filter($item['children'], fn($c) => !isset($c['block_type']) || (isset($c['json']['data']['type']) && \Container::get('\MSFramework\Frontend\visualBuilder')->getBlocksDetails($c['json']['data']['type'])) ); // Re-index array dopo il filtro per evitare JSON object invece di array $item['children'] = array_values($item['children']); } // Rimuovo i numeri ridondanti sopra le icone if (($item['type'] ?? '') === 'col' && !empty($item['children'])) { $newChildren = []; $skipNext = false; $count = count($item['children']); for ($i = 0; $i < $count; $i++) { $currentBlock = $item['children'][$i]; $nextBlock = $item['children'][$i+1] ?? null; $isRedundantNumber = false; // Controlliamo se il blocco corrente è un Titolo $bType = $currentBlock['block_type'] ?? ($currentBlock['json']['data']['type'] ?? ''); if ($bType === 'main/title' && $nextBlock) { $nextType = $nextBlock['block_type'] ?? ($nextBlock['json']['data']['type'] ?? ''); if ($nextType === 'main/icon') { $text = trim(strip_tags($currentBlock['json']['data']['text'] ?? '')); if (preg_match('/^(step\s|fase\s)?([0-9]+|[IVX]+)[\.]?$/i', $text)) { $isRedundantNumber = true; } } } if ($isRedundantNumber) { continue; } $newChildren[] = $currentBlock; } $item['children'] = $newChildren; } // Converto eventuali margini in padding per non perdere fli sfondi if (($item['type'] ?? '') === 'col' && !empty($item['children'])) { $count = count($item['children']); for ($i = 0; $i < $count - 1; $i++) { // Usiamo riferimenti (&) per modificare direttamente l'array originale $blockA = &$item['children'][$i]; $blockB = &$item['children'][$i+1]; // Recuperiamo i colori di sfondo $bgA = $blockA['json']['design']['background']['color'] ?? null; $bgB = $blockB['json']['design']['background']['color'] ?? null; // Se entrambi hanno un background ed è LO STESSO colore (e non è trasparente/bianco) if ($bgA && $bgB && $bgA === $bgB && $bgA !== 'transparent' && $bgA !== '#FFFFFF' && $bgA !== '#ffffff') { // 1. Gestione Margin Bottom del Blocco A (Sposta in Padding Bottom) $marginBottom = intval($blockA['json']['design']['margin']['bottom'] ?? 0); if ($marginBottom > 0) { $currentPadding = intval($blockA['json']['design']['padding']['bottom'] ?? 0); // Sposta il valore $blockA['json']['design']['padding']['bottom'] = (string)($currentPadding + $marginBottom); $blockA['json']['design']['padding']['suffix'] = 'px'; // Sicurezza // Azzera il margine $blockA['json']['design']['margin']['bottom'] = '0'; } // 2. Gestione Margin Top del Blocco B (Sposta in Padding Top) // (L'AI lo fa raramente, ma per sicurezza copriamo anche questo caso) $marginTop = intval($blockB['json']['design']['margin']['top'] ?? 0); if ($marginTop > 0) { $currentPadding = intval($blockB['json']['design']['padding']['top'] ?? 0); // Sposta il valore $blockB['json']['design']['padding']['top'] = (string)($currentPadding + $marginTop); $blockB['json']['design']['padding']['suffix'] = 'px'; // Azzera il margine $blockB['json']['design']['margin']['top'] = '0'; } } // Rilasciamo i riferimenti per evitare bug nel loop successivo unset($blockA, $blockB); } } return $item; }, $jsonPhase3, true); } /** * Rigenera il contenuto interno delle slide del carosello usando l'AI. * Include sistema di CACHE per evitare chiamate ripetute. */ protected function _regenerateCarouselSlides($tabs, $visualIdentity, $mode = 'generate') { // 1. Preparazione input $slidesToGenerate = []; foreach ($tabs as $key => $tab) { $rawData = $tab['data'] ?? ''; // Se è un JSON (anche se malformato strutturalmente per il builder), proviamo a decodificarlo // per passarlo pulito al prompt. Altrimenti passiamo la stringa grezza. $contextContent = $rawData; if (is_string($rawData)) { $decoded = json_decode($rawData, true); if ($decoded) { $contextContent = $decoded; } } $slidesToGenerate[$key] = [ 'title' => $tab['title'] ?? "Slide {$key}", 'original_content' => $contextContent // Passiamo il contesto sporco all'AI ]; } if (empty($slidesToGenerate)) return $tabs; // 2. Caricamento Dati Strutturali (Schema + Esempi) $this->_loadReferenceFiles(); $blocksRefText = ""; if ($this->blocksReference) { $blocksRefText = json_encode($this->blocksReference, JSON_PRETTY_PRINT); } // --- SISTEMA CACHE --- // L'hash ora include anche il contenuto originale e la modalità $cacheId = 'carousel_v7_' . md5(json_encode($slidesToGenerate) . json_encode($visualIdentity) . $mode); $generatedContent = $this->checkCache($cacheId); if ($generatedContent) { $this->logMessage("[Carousel] Contenuto recuperato da CACHE ({$mode}): {$cacheId}"); } else { $visualJson = json_encode($visualIdentity, JSON_PRETTY_PRINT); $slidesJson = json_encode($slidesToGenerate, JSON_PRETTY_PRINT); // Variabili per il prompt $systemPrompt = ""; $userPrompt = ""; $config = []; // --- BIVIO LOGICO: EDIT vs GENERATE --- if ($mode === 'edit') { // ========================================== // MODALITÀ EDIT: COPYWRITING + PRESERVAZIONE DESIGN // ========================================== $systemPrompt = << Col -> Block. Non aggiungere o rimuovere blocchi se non strettamente necessario. 4. **IMMAGINI:** Se ci sono immagini (`files`), NON toccarle. **OUTPUT:** Restituisci un JSON con le stesse chiavi dell'input (`slide_key`: [rows...]). All'interno, i testi devono essere migliorati, ma il `design` deve essere identico all'originale. PROMPT; $userPrompt = "Migliora i testi di queste slide mantenendo il design originale:\n{$slidesJson}"; $config = ['temperature' => 0.4, 'topP' => 0.95, 'jsonMode' => true]; } else { // ========================================== // MODALITÀ GENERATE: LOGICA ORIGINALE INVARIATA // ========================================== $bestExamples = $this->_selectBestExamples('page', 'slide content testimonial feature card small content'); $examplesText = $this->_formatExamplesForPrompt($bestExamples); $systemPrompt = << Col -> Block):** Analizza come sono annidati i blocchi in questi esempi e replica la struttura. ``` {$examplesText} ``` **OBIETTIVO:** Per ogni slide richiesta, analizza `original_content`. Estrai i testi, il significato e l'intento da lì e inseriscili in una NUOVA struttura JSON valida. La struttura DEVE essere gerarchica: **ROW -> CHILDREN (Cols) -> CHILDREN (Blocks)**. **FORMATO OUTPUT RICHIESTO (JSON Unico):** { "slide_key_1": [ { "type": "row", "children": [ { "type": "col", "col": 12, "children": [ { "type": "block", "block_type": "main/title", "json": { "data": { "text": "Titolo Estratto..." } } }, { "type": "block", "block_type": "main/text-editor", "json": { "data": { "content": "Testo Estratto..." } } } ] } ] } ], ... } **REGOLE CRITICHE:** 1. **FONTE DATI:** Usa i testi presenti in `original_content`. Se il contenuto originale è rotto o HTML grezzo, puliscilo ma mantieni il significato. 2. **STRUTTURA:** Ignora la struttura del contenuto originale. Riscrivila da zero usando Row -> Col -> Block. 3. **DATI:** Usa `json.data` per i contenuti (testi, link) come visto nello schema. 4. **COERENZA:** Il contenuto deve rispecchiare il Titolo della slide fornito. 5. **RECENSIONI:** Se il titolo suggerisce una recensione, usa `main/rating` (se disponibile nello schema) o icone stella. Restituisci SOLO il JSON valido. PROMPT; $userPrompt = "Ripara e struttura il contenuto per queste slide:\n{$slidesJson}"; $config = ['temperature' => 0.3, 'topP' => 0.95, 'jsonMode' => true]; } try { $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $generatedContent = json_decode($this->_cleanJsonResponse($response->text()), true); if (json_last_error() === JSON_ERROR_NONE && is_array($generatedContent)) { $this->saveCache($cacheId, $generatedContent); } else { $generatedContent = null; } } catch (\Exception $e) { $this->logMessage("Eccezione generazione slide: " . $e->getMessage()); $generatedContent = null; } } // 4. Assegnazione e Post-Processing if ($generatedContent && is_array($generatedContent)) { foreach ($tabs as $key => &$tab) { if (isset($generatedContent[$key])) { $rawSlideRows = $generatedContent[$key]; // FIX: Gestione Wrapper 'original_content' se presente (tipico errore AI in edit mode) if (isset($rawSlideRows['original_content']) && is_array($rawSlideRows['original_content'])) { $rawSlideRows = $rawSlideRows['original_content']; } // Normalizzazione array if (isset($rawSlideRows['type']) && $rawSlideRows['type'] === 'row') { $rawSlideRows = [$rawSlideRows]; } // Post-process ricorsivo passando la modalità corretta $processedSlideRows = $this->_postProcessJson($rawSlideRows, $mode); $tab['data'] = json_encode($processedSlideRows); } } } return $tabs; } /** * Rigenera il contenuto interno di Tabs e Accordion usando l'AI. * Identico a _regenerateCarouselSlides ma ottimizzato per contenuti informativi. */ protected function _regenerateTabContent($tabsItems, $visualIdentity, $mode = 'generate') { // 1. Preparazione input $itemsToGenerate = []; foreach ($tabsItems as $key => $tab) { $rawData = $tab['data'] ?? ''; // Decodifica preliminare per dare contesto all'AI $contextContent = $rawData; if (is_string($rawData)) { $decoded = json_decode($rawData, true); if ($decoded) $contextContent = $decoded; } $itemsToGenerate[$key] = [ 'title' => $tab['title'] ?? "Tab {$key}", 'original_content' => $contextContent ]; } if (empty($itemsToGenerate)) return $tabsItems; // 2. Caricamento Riferimenti $this->_loadReferenceFiles(); $blocksRefText = ""; if ($this->blocksReference) { $blocksRefText = json_encode($this->blocksReference, JSON_PRETTY_PRINT); } // Cache Key $cacheId = 'tabs_regen_v7_' . md5(json_encode($itemsToGenerate) . json_encode($visualIdentity) . $mode); $generatedContent = $this->checkCache($cacheId); if ($generatedContent) { $this->logMessage("[Tabs] Contenuto recuperato da CACHE ({$mode}): {$cacheId}"); } else { $visualJson = json_encode($visualIdentity, JSON_PRETTY_PRINT); $itemsJson = json_encode($itemsToGenerate, JSON_PRETTY_PRINT); // Variabili per il prompt $systemPrompt = ""; $userPrompt = ""; $config = []; // --- BIVIO LOGICO: EDIT vs GENERATE --- if ($mode === 'edit') { // ========================================== // MODALITÀ EDIT: COPYWRITING + PRESERVAZIONE DESIGN // ========================================== $systemPrompt = << 0.4, 'topP' => 0.95, 'jsonMode' => true]; } else { // ========================================== // MODALITÀ GENERATE: LOGICA ORIGINALE INVARIATA // ========================================== // Selezioniamo esempi orientati ai contenuti (features, text, lists) $bestExamples = $this->_selectBestExamples('features', 'detailed list info card'); $examplesText = $this->_formatExamplesForPrompt($bestExamples); $systemPrompt = << Col -> Block):** ``` {$examplesText} ``` **OBIETTIVO:** Per ogni tab richiesta, analizza `original_content`. Estrai le informazioni (testi, orari, indirizzi, liste) e crea una NUOVA struttura JSON pulita e stilizzata. **REGOLE:** 1. **STRUTTURA:** La root deve essere un array di oggetti ROW. Gerarchia: Row -> Children (Cols) -> Children (Blocks). 2. **STILE:** Usa i colori e i font del Contesto Visivo. NON usare stili inline HTML (es. `

`). Usa `json.design`. 3. **CONTENUTO:** - Se vedi orari o liste, usa `main/ordered-list` o `main/icon` + `main/text-editor`. - Se vedi indirizzi, usa `main/google-maps` se appropriato. - Se vedi titoli, usa `main/title`. **FORMATO OUTPUT (JSON Unico):** { "key_della_tab": [ { "type": "row", "children": [ ... ] } ], ... } Rispondi SOLO con il JSON valido. PROMPT; $userPrompt = "Ristruttura il contenuto per queste tab:\n{$itemsJson}"; $config = ['temperature' => 0.3, 'topP' => 0.95, 'jsonMode' => true]; } try { $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $generatedContent = json_decode($this->_cleanJsonResponse($response->text()), true); if (json_last_error() === JSON_ERROR_NONE && is_array($generatedContent)) { $this->saveCache($cacheId, $generatedContent); } else { $generatedContent = null; } } catch (\Exception $e) { $this->logMessage("Eccezione generazione tabs: " . $e->getMessage()); $generatedContent = null; } } // 4. Assegnazione if ($generatedContent && is_array($generatedContent)) { foreach ($tabsItems as $key => &$tab) { if (isset($generatedContent[$key])) { $rawRows = $generatedContent[$key]; // FIX: Gestione Wrapper 'original_content' se presente if (isset($rawRows['original_content']) && is_array($rawRows['original_content'])) { $rawRows = $rawRows['original_content']; } // Normalizzazione if (isset($rawRows['type']) && $rawRows['type'] === 'row') { $rawRows = [$rawRows]; } // Post-process ricorsivo passando la modalità corretta $processedRows = $this->_postProcessJson($rawRows, $mode); // Salviamo come stringa JSON (formato richiesto dal blocco tabs) $tab['data'] = json_encode($processedRows); } } } return $tabsItems; } /** * Restyling intelligente delle strutture dei plugin (Loop, Product Card, etc.) * Mantiene i blocchi funzionali ma aggiorna design e colori in base al tema. */ protected function _regeneratePluginStructure($structureRows, $visualIdentity, $pluginType) { if (empty($structureRows) || empty($visualIdentity)) { return $structureRows; } // 1. Preparazione Contesto // Serializziamo la struttura originale per passarla come riferimento $structureJson = json_encode($structureRows, JSON_PRETTY_PRINT); $visualJson = json_encode($visualIdentity, JSON_PRETTY_PRINT); // Carichiamo anche i blocchi di riferimento per aiutare l'AI a non allucinare chiavi $this->_loadReferenceFiles(); $blocksRefText = ""; if ($this->blocksReference) { // Passiamo solo i common_fields per risparmiare token, ci serve sapere come stilizzare $schemaSubset = ['common_fields' => $this->blocksReference['common_fields'] ?? []]; $blocksRefText = json_encode($schemaSubset, JSON_PRETTY_PRINT); } // 2. Cache Key (Hash della struttura + identità visiva) $cacheId = 'plugin_restyle_' . md5($structureJson . $visualJson . $pluginType); // Check Cache $cachedResult = $this->checkCache($cacheId); if ($cachedResult) { $this->logMessage("[Plugin Restyle] Recuperato da CACHE: {$pluginType}"); return $cachedResult; } // 3. Costruzione Prompt $systemPrompt = << Col -> Block esistente. L'unica eccezione è se devi avvolgere elementi per layout complessi, ma preferibilmente mantieni lo scheletro. 2. **DESIGN:** Aggiorna `json.design` di ogni elemento: - Usa i colori della palette (`primary`, `secondary`, `background`) per sfondi, testi e bordi. - Applica i font corretti in `json.design.typography` (uas dei font leggibili). - Aggiungi `padding`, `margin`, `border-radius` coerenti con lo stile (es. se lo stile è "rounded", usa radius alti). - Assicurati che il contrasto testo/sfondo sia leggibile. 3. **CONTENUTO:** Aggiorna i testi placeholder (`json.data.text` o `content`) rendendoli professionali e coerenti con lo stile, ma mantienili generici (es. "Titolo Articolo" invece di "Hello World"). 4. **SCHEMA:** Usa solo chiavi CSS valide per il builder (vedi schema allegato). **CONTESTO VISIVO:** {$visualJson} **SCHEMA CSS VALIDO (Reference):** {$blocksRefText} Rispondi SOLO con il JSON della struttura aggiornata. PROMPT; $userPrompt = << 0.4, 'topP' => 0.9, 'jsonMode' => true]; // Temp bassa per preservare struttura $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $generatedJson = json_decode($this->_cleanJsonResponse($response->text()), true); if (json_last_error() === JSON_ERROR_NONE && is_array($generatedJson)) { // Normalizzazione: se l'AI restituisce un oggetto singolo invece di array di righe if (isset($generatedJson['type']) && $generatedJson['type'] === 'row') { $generatedJson = [$generatedJson]; } elseif (isset($generatedJson['rows'])) { $generatedJson = $generatedJson['rows']; } $this->saveCache($cacheId, $generatedJson); return $generatedJson; } else { $this->logMessage("[Plugin Restyle] Errore JSON AI: " . json_last_error_msg()); } } catch (\Exception $e) { $this->logMessage("[Plugin Restyle] Eccezione: " . $e->getMessage()); } // Fallback: ritorna l'originale se qualcosa va storto return $structureRows; } protected function _generateImageFromData($item) { // 1. DETERMINAZIONE PROPORZIONE $proportion = '4:3'; if (isset($item['ai-proportion']) && !empty($item['ai-proportion'])) { $proportion = $item['ai-proportion']; } elseif (isset($item['ai-orientation'])) { $orient = strtolower($item['ai-orientation']); if (str_contains($orient, 'land')) $proportion = '16:9'; else if (str_contains($orient, 'port')) $proportion = '3:4'; else if (str_contains($orient, 'squ')) $proportion = '1:1'; } elseif (isset($item['type']) && $item['type'] === 'hero') { $proportion = '16:9'; } // 2. DETERMINAZIONE BACKGROUND MODE $bgMode = $item['ai-background'] ?? 'standard'; // 3. GENERAZIONE if ($img = $this->_generateAIImage($item['ai-query'], $proportion, $bgMode)) { $fileStructure = [[$img['directory'], $img['filename']]]; return json_encode($fileStructure); } return ''; } protected function _convertTransparentBackgroundToOverlay(&$design) { // Controlla se esiste background color con rgba if (isset($design['background']['color']) && is_string($design['background']['color'])) { $colorVal = $design['background']['color']; // Regex per catturare: rgba(R, G, B, Alpha) if (preg_match('/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9\.]+)\s*\)/i', $colorVal, $matches) && !str_contains($colorVal, 'gradient')) { $r = $matches[1]; $g = $matches[2]; $b = $matches[3]; $alpha = (float)$matches[4]; // Es: 0.05 // 1. Converti RGB a HEX $hex = sprintf("#%02x%02x%02x", $r, $g, $b); // 2. Converti Alpha a Opacity (0-100) // Es: 0.05 * 100 = 5 $opacity = (string)round($alpha * 100); // 3. Imposta Overlay if (!isset($design['overlay'])) $design['overlay'] = []; $design['overlay']['color'] = $hex; $design['overlay']['opacity'] = $opacity; // 4. Rimuovi background color originale (altrimenti si sovrappone o confonde) $design['background']['color'] = ''; } } } protected function _sanitizeDesignColors(&$designArray) { if (!is_array($designArray)) return; $cleaner = function(&$data, $currentKey = '') use (&$cleaner) { foreach ($data as $key => &$value) { if (is_array($value)) { // Caso: Background Image contiene Gradiente -> Sposta in Color if ($key === 'background' && isset($value['image']) && is_string($value['image'])) { if (preg_match('/(linear|radial)-gradient/i', $value['image'])) { $value['color'] = $value['image']; $value['image'] = ''; } } $cleaner($value, $key); } elseif (is_string($value)) { if (str_contains($value, 'gradient')) { $pattern = '/radial-gradient\(\s*[\d\.]+%(\s+[\d\.]+%?)?\s+at/i'; $replacement = 'radial-gradient(ellipse farthest-side at'; $value = preg_replace($pattern, $replacement, $value); } else if (str_contains($value, 'rgb')) { $value = $this->_rgbToHex($value); } } } }; $cleaner($designArray); } protected function _rgbToHex($string) { if (preg_match('/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/', $string, $matches)) { return sprintf("#%02x%02x%02x", $matches[1], $matches[2], $matches[3]); } return $string; } protected function _fixColumnWidths($rows) { foreach ($rows as &$row) { if (($row['type'] ?? '') !== 'row' || empty($row['children'])) continue; $childrenCount = count($row['children']); // FASE 1: Pulizia e Normalizzazione Dati foreach ($row['children'] as &$col) { // Rimuovi offset espliciti (vogliamo griglia piena) if (isset($col['offset'])) unset($col['offset']); if (isset($col['json']['responsive']['desktop']['offset'])) { unset($col['json']['responsive']['desktop']['offset']); } // Leggi larghezza (default a 12 se manca) $width = intval($col['json']['responsive']['desktop']['col'] ?? $col['col'] ?? 0); // Safety: se larghezza 0 o nulla, assegnamo una stima if ($width <= 0) $width = floor(12 / ($childrenCount > 0 ? $childrenCount : 1)); if ($width < 1) $width = 1; if ($width > 12) $width = 12; // Salviamo in variabile temporanea $col['__temp_width'] = $width; } unset($col); // Rompi riferimento // FASE 2: Logica Wrapping Intelligente $lineTotal = 0; $lineStartIndex = 0; foreach ($row['children'] as $k => &$col) { $width = $col['__temp_width']; // Verifichiamo se l'aggiunta di questa colonna fa "traboccare" la riga ( > 12 ) if ($lineTotal + $width > 12) { if ($lineTotal < 12 && $k > 0) { $prevIndex = $k - 1; $missing = 12 - $lineTotal; // Aggiorniamo la colonna precedente if (isset($row['children'][$prevIndex])) { $this->_updateColData($row['children'][$prevIndex], $row['children'][$prevIndex]['col'] + $missing); } } // RESETTA per la nuova riga $lineTotal = $width; $lineStartIndex = $k; // Inizio nuova riga visiva } else { // C'è ancora spazio nella riga corrente $lineTotal += $width; } // Applico la larghezza corrente (provvisoria, potrebbe essere modificata se è l'ultima della riga) $this->_updateColData($col, $width); // Se è l'assolutamente ULTIMA colonna dell'array if ($k === $childrenCount - 1) { // Se l'ultima riga visiva non arriva a 12, allarghiamo quest'ultima colonna if ($lineTotal < 12) { $missing = 12 - $lineTotal; $this->_updateColData($col, $width + $missing); } } // Pulizia temp unset($col['__temp_width']); } unset($col); } return $rows; } /** * UPDATE Issue 4: Fixer per strutture complesse (Carousel/Tabs). * Assicura che il contenuto interno segua la gerarchia Row -> Col -> Block. */ protected function _fixComplexBlockStructures($blockItem) { $bType = $blockItem['json']['data']['type']; $targetKey = 'tabs'; if ($bType === 'main/accordion') $targetKey = 'items'; if (!isset($blockItem['json']['data'][$targetKey]) || !is_array($blockItem['json']['data'][$targetKey])) { return $blockItem; } foreach ($blockItem['json']['data'][$targetKey] as &$tab) { if (empty($tab['data'])) continue; $content = $tab['data']; if (is_string($content)) { $decoded = json_decode($content, true); if ($decoded) $content = $decoded; } if (!is_array($content)) continue; // Check se la radice è Row $isRowStructure = false; if (isset($content[0]['type']) && $content[0]['type'] === 'row') { $isRowStructure = true; } elseif (isset($content['type']) && $content['type'] === 'row') { $isRowStructure = true; $content = [$content]; } // SE NON È ROW, AVVOLGIAMO if (!$isRowStructure) { $wrapper = [ [ 'type' => 'row', 'children' => [ [ 'type' => 'col', 'col' => 12, 'children' => [] ] ], 'json' => ['data' => ['type' => 'hidden/row']] ] ]; if (isset($content['type']) || isset($content['block_type'])) { $content = [$content]; } $wrapper[0]['children'][0]['children'] = $content; $tab['data'] = json_encode($wrapper); } else { if (is_array($tab['data'])) { $tab['data'] = json_encode($tab['data']); } } } return $blockItem; } private function _updateColData(&$col, $width) { $width = intval($width); if ($width > 12) $width = 12; // Safety cap $col['col'] = $width; if (!isset($col['json']['responsive']['desktop'])) { $col['json']['responsive']['desktop'] = []; } $col['json']['responsive']['desktop']['col'] = $width; // Assicuriamoci che non ci siano offset unset($col['json']['responsive']['desktop']['offset']); } /** * Ripara una stringa JSON malformata o converte HTML grezzo in struttura blocchi valida tramite AI. * Include sistema di caching per evitare chiamate ripetute su stessi errori. */ protected function _repairMalformedJsonContent($malformedContent) { if (empty($malformedContent)) return null; // 1. CACHE CHECK // Creiamo un hash del contenuto rotto per identificarlo univocamente $contentHash = md5($malformedContent); $cacheFile = $this->cacheDir . '/fix_json_' . $contentHash . '.json'; if (file_exists($cacheFile)) { $this->logMessage(" -> Recuperata riparazione JSON da cache: {$contentHash}"); return file_get_contents($cacheFile); } $systemPrompt = << 0.1, 'topP' => 0.95, 'topK' => 20, 'jsonMode' => true]; // Usiamo il client AI già istanziato nella classe base $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $fixedContent = $this->_cleanJsonResponse($response->text()); // 4. VALIDAZIONE if (json_decode($fixedContent) !== null) { file_put_contents($cacheFile, $fixedContent); return $fixedContent; } else { $this->logMessage(" -> L'AI ha restituito un JSON ancora invalido."); } } catch (\Exception $e) { $this->logMessage(" -> ERRORE API durante riparazione JSON: " . $e->getMessage()); } return null; } // =================================================================== // PUBLIC API - GENERATE PATTERNS // =================================================================== public function generatePatterns() { echo "=== GENERAZIONE PATTERN v15 ===\n"; $examples = [ 'blocks_list' => [], 'common_structures' => [] ]; foreach (\Container::get('\MSFramework\AI\Templates\elements')->getElements('page', ['static-home-page', 'landing-page', 'sales-page', 'vsl-page'], false) as $element) { try { foreach ($element['element_content'] as $contentKey => $single) { $visualBuilderString = str_replace(['', ''], '', $single['data'] ?? ''); $jsonArray = json_decode($visualBuilderString, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($jsonArray)) continue; $cleanJsonArray = array_filter($jsonArray, fn($item) => ($item['type'] ?? '') !== 'settings'); if (empty($cleanJsonArray)) continue; array_walk_recursive($cleanJsonArray, function(&$item) use (&$examples) { if (!is_array($item)) return $item; if (isset($item['ai-query'])) { $item['ai-query'] = "Detailed description in English"; $item['ai-proportion'] = "4:3"; $item['ai-background'] = "standard"; } if (isset($item['image']['files']) && is_array($item['image']['files'])) { $item['image']['files'] = [["PAGEGALLERY", "example-image.jpg"]]; } if (isset($item['tabs'])) { $item['tabs'] = ["unique_key_01" => ["title" => "Titolo TAB", "data" => ""]]; } if (!empty($item['block_type']) && isset($item['json'])) { if (str_contains($item['block_type'], 'plugins')) return $item; if ($item['block_type'] === 'main/title') { $item['json']['data']['text'] = "Inserire qui testo"; unset($item['json']['data']['content']); } if ($item['block_type'] === 'main/text-editor') { unset($item['json']['data']['text']); $item['json']['data']['content'] = "

Inserire qui contenuto HTML

"; } $examples['blocks_list'][$item['block_type']] = array_replace_recursive($examples['blocks_list'][$item['block_type']] ?? [], $item); } if (isset($item['type']) && ($item['type'] === 'col' || $item['type'] === 'row')) { $tmpSave = $item; $tmpSave['children'] = []; $examples['blocks_list'][$item['type']] = array_replace_recursive($examples['blocks_list'][$item['type']] ?? [], $tmpSave); } return $item; }, $cleanJsonArray, true); } } catch (\Exception $e) { continue; } } $this->_savePatternFile($examples, 'fw360-importer-visualbuilder-blocks.json'); foreach (\Container::get('\MSFramework\AI\Templates\landingpages')->getDetails() as $landingPage) { try { $pattern = [ 'pattern_name' => "Landing Page: {$landingPage['element_title']}", 'description' => "Esempio completo di landing page per il settore {$landingPage['element_title']}", 'design_tags' => ["landing page", strtolower($landingPage['element_title'])], 'visual_builder_json' => [] ]; $content = json_decode($landingPage['element_content'], true); if (!is_array($content)) continue; $allRows = []; foreach ($content as $single) { $visualBuilderString = str_replace(['', ''], '', $single['data'] ?? ''); $jsonRows = json_decode($visualBuilderString, true); if (json_last_error() === JSON_ERROR_NONE && is_array($jsonRows)) { $allRows = array_merge($allRows, $jsonRows); } } if (!empty($allRows)) { $pattern['visual_builder_json'] = [['rows' => $allRows]]; $slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $landingPage['element_title']))); $fileName = sprintf("fw360-importer-pattern-landingpage-%d-%s.json", $landingPage['id'], $slug); $this->_savePatternFile($pattern, $fileName); } } catch (\Exception $e) { continue; } } echo "\n=== GENERAZIONE COMPLETATA ===\n"; return true; } private function _savePatternFile($data, $filename) { $filePath = $this->patternsDir . '/' . $filename; $jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if (json_last_error() === JSON_ERROR_NONE) { file_put_contents($filePath, $jsonContent); echo "Pattern salvato: " . $filename . "\n"; $this->logMessage("Salvato pattern: {$filename}"); } else { echo "ERRORE: Impossibile generare JSON per " . $filename . ". Errore: " . json_last_error_msg() . "\n"; $this->logMessage("ERRORE salvataggio pattern {$filename}: " . json_last_error_msg()); } } // =================================================================== // LIBRERIA - SALVATAGGIO/CARICAMENTO // =================================================================== public function saveToLibrary($type, $title, $content) { \Container::get('\MSFramework\database')->insert("ai__generations", [ 'type' => $type, 'title' => $title, 'content' => $content, ]); // Mantengo solo gli ultimi 10 if(($lastID = \Container::get('\MSFramework\database')->getAssoc("SELECT id FROM ai__generations WHERE type = :type ORDER BY id DESC LIMIT 10, 1", [':type' => $type], true)['id']) > 0) { \Container::get('\MSFramework\database')->query("DELETE FROM `ai__generations` WHERE type = :type AND id < :last_id", [':type' => $type, ':last_id' => $lastID]); } } public function getFromLibrary($id = 0, $type = []) { \Container::get('\MSFramework\Updater\dbUpdater')->runUpdate("generatorTables", function() { \Container::get('\MSFramework\database')->query("CREATE TABLE `ai__generations` (`id` INT NOT NULL AUTO_INCREMENT , `type` VARCHAR(15) NOT NULL , `title` TEXT NOT NULL , `content` LONGTEXT NOT NULL , `creation_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (`id`)) ENGINE = InnoDB;"); }); if($id) { return \Container::get('\MSFramework\database')->getAssoc("SELECT * FROM ai__generations WHERE id = :id", [':id' => $id], true); } // Ritorno gli ultimi 10 $history = []; foreach(\Container::get('\MSFramework\database')->getAssoc("SELECT id, title, creation_date FROM ai__generations WHERE type IN(" . implode(',', array_map(function($r) { return \Container::get('\MSFramework\database')->quote($r); }, $type)) . ") ORDER BY id DESC LIMIT 20", []) as $r) { $history[$r['id']] = $r; } return $history; } // =================================================================== // COSTI - CALCOLO COSTI API // =================================================================== // Metodo helper chiamato automaticamente dal Proxy public function trackUsage($usageMetadata) { $this->totalTokensUsed += $usageMetadata->totalTokenCount ?? 0; $this->inputTokensUsed += $usageMetadata->promptTokenCount ?? 0; $this->outputTokensUsed += $usageMetadata->candidatesTokenCount ?? 0; } // Metodo per ottenere il totale (da chiamare alla fine) public function getRealCost() { // 1. calcolo costi testo $costTextInput = ($this->inputTokensUsed / 1000000) * self::PRICE_TEXT_INPUT_PER_1M; $costTextOutput = ($this->outputTokensUsed / 1000000) * self::PRICE_TEXT_OUTPUT_PER_1M; // 2. Calcolo Costi Immagini (GPT - in da convertire) $costImgInput = ($this->imgInputTokensUsed / 1000000) * self::PRICE_IMG_INPUT_PER_1M; $costImgOutput = ($this->imgOutputTokensUsed / 1000000) * self::PRICE_IMG_OUTPUT_PER_1M; // Tottali $totalText = ($costTextInput + $costTextOutput); $totalImg = ($costImgInput + $costImgOutput); // Moltiplico i costi if((string)\Session::$items['userData']['userlevel'] !== "0" || (string)\Session::$items['userData']['global'] !== "1") { $totalText *= self::PRICE_CREDIT_MULTIPLIER; $totalImg *= self::PRICE_CREDIT_MULTIPLIER; } // Totale $grandTotal = $totalText + $totalImg; return [ 'currency' => '', 'total' => round($grandTotal, 5), 'formatted_total' => \Container::get('\MSFramework\Fatturazione\imposte')->formatPrice($grandTotal, ''), 'breakdown' => [ 'text' => [ 'tokens_in' => $this->inputTokensUsed, 'tokens_out' => $this->outputTokensUsed, 'cost' => round($totalText, 5), 'formatted_cost' => \Container::get('\MSFramework\Fatturazione\imposte')->formatPrice($totalText, ''), ], 'images' => [ 'count' => $this->generatedImagesCount, 'tokens_in' => $this->imgInputTokensUsed, 'tokens_out' => $this->imgOutputTokensUsed, 'cost' => round($totalImg, 5), 'formatted_cost' => \Container::get('\MSFramework\Fatturazione\imposte')->formatPrice($totalImg, '') ] ] ]; } } ----- File: editor.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\editor.php ---------------------------------------- _loadReferenceFiles(); if (empty($fullPageRows)) { $fullPageRows = $rows; } $isColumnContext = false; $isBlockContext = false; if (!empty($rows) && isset($rows[0]['type']) && $rows[0]['type'] === 'col') { $isColumnContext = true; } else if (!empty($rows) && isset($rows[0]['type']) && $rows[0]['type'] === 'block') { $isBlockContext = true; } $this->logMessage("=== INIZIO EDITING STRUTTURALE (Unified Generator v2.6 con Step Dinamici) ==="); if ($isColumnContext) $this->logMessage("-> Modalità Contesto: COLONNA"); $wizard = \Container::get('\MSFramework\Framework\wizard'); $wizard->setUpdates("{{Analizzo la pagina}}", "{{Studio la struttura e il contesto...}}", self::WIZARD_ICON); $this->extractedVisualIdentity = $this->_extractVisualIdentityFromRows($fullPageRows); $this->sectionContext['visualIdentity'] = $this->extractedVisualIdentity; $this->globalVisualIdentity = ['visualIdentity' => $this->extractedVisualIdentity]; $pageSummary = $this->_summarizePageStructure($fullPageRows); $contextKeywords = []; foreach ($pageSummary as $section) { if (!empty($section['keywords'])) { $contextKeywords = array_merge($contextKeywords, array_slice($section['keywords'], 0, 3)); } } $semanticContext = implode(" | ", array_unique(array_slice($contextKeywords, 0, 50))); $this->logMessage("Contesto Semantico Rilevato: " . substr($semanticContext, 0, 100) . "..."); $wizard->setUpdates("{{Interpreto la richiesta}}", "{{L'AI sta analizzando il tuo obiettivo...}}", self::WIZARD_ICON); $intentData = $this->_analyzeUserIntent($editPrompt, $rows, $semanticContext); $refinedPrompt = $intentData['refined_prompt'] ?? $editPrompt; $structureSummary = $this->_summarizePageStructure($rows); $actionPlan = $this->_generateActionPlan($structureSummary, $refinedPrompt); // ================================================================================= // MODIFICA PRINCIPALE: GESTIONE CONTEGGIO STEP DINAMICO // ================================================================================= $isSimpleEdit = (count($actionPlan) === 1 && in_array($actionPlan[0]['action'], ['EDIT', 'KEEP'])); if (!$isSimpleEdit) { // Modalità Complessa: usiamo il conteggio step come prima. $totalSteps = count($actionPlan); $wizard->setUpdateSteps(1, $totalSteps + 2); // +2 per assemblaggio e finalizzazione/immagini } else { // Modalità Semplice: non impostiamo un totale, il wizard mostrerà solo messaggi di stato. // La chiamata a setUpdateSteps('+1') non avrà effetto sul contatore visivo. } // ================================================================================= $finalRows = []; // --- CICLO DI ESECUZIONE AZIONI --- foreach ($actionPlan as $stepIndex => $action) { $specificPrompt = !empty($action['prompt_slice']) ? $action['prompt_slice'] : $refinedPrompt; $sectionType = $action['type_hint'] ?? 'generic'; $combinedContext = "ARGOMENTO SITO: " . $semanticContext . ". \nOBIETTIVO UTENTE: " . $specificPrompt; $virtualSectionData = [ 'type' => $sectionType, 'purpose' => $specificPrompt, 'contentGuidelines' => "Esegui rigorosamente la richiesta utente: " . $specificPrompt, 'cols' => 12 ]; if ($action['action'] === 'EDIT') { $idx = $action['original_index']; if (isset($rows[$idx])) { $strategy = "Applico modifiche: " . substr($specificPrompt, 0, 40) . "..."; $wizard->setUpdates("{{Modifico sezione}}", "{{{$strategy}}}", self::WIZARD_ICON); $existingRowJson = json_encode([$rows[$idx]], JSON_PRETTY_PRINT); $modifiedResult = $this->_generateSectionWithRetry( $virtualSectionData, $this->sectionContext['visualIdentity'], 'Italiano', $wizard, $stepIndex + 1, count($actionPlan), $combinedContext, $existingRowJson ); if (!isset($modifiedResult['error']) && is_array($modifiedResult)) { foreach($modifiedResult as $mr) $finalRows[] = $mr; } else { $finalRows[] = $rows[$idx]; $this->logMessage("Errore modifica sezione $idx: " . ($modifiedResult['error'] ?? 'unknown')); } } } else if ($action['action'] === 'GENERATE') { $wizard->setUpdates("{{Creo nuova sezione}}", "{{Creo: " . substr($specificPrompt, 0, 40) . "...}}", self::WIZARD_ICON); $newResult = $this->_generateSectionWithRetry( $virtualSectionData, $this->sectionContext['visualIdentity'], 'Italiano', $wizard, $stepIndex + 1, count($actionPlan), $combinedContext, null ); if (!isset($newResult['error']) && is_array($newResult)) { foreach($newResult as $nr) $finalRows[] = $nr; } } else if ($action['action'] === 'KEEP') { $idx = $action['original_index']; if (isset($rows[$idx])) { $finalRows[] = $rows[$idx]; } } if (!$isSimpleEdit) { $wizard->setUpdateSteps('+1'); } } // --- POST-PROCESSING FINALE: NORMALIZZAZIONE STRUTTURALE --- if ($isColumnContext && !empty($finalRows)) { $primaryCol = null; $orphans = []; foreach ($finalRows as $item) { if (($item['type'] ?? '') === 'settings') continue; if ($primaryCol === null && ($item['type'] ?? '') === 'col') { $primaryCol = $item; continue; } $orphans[] = $item; } if ($primaryCol) { if (!empty($orphans)) { if (!isset($primaryCol['children'])) $primaryCol['children'] = []; $primaryCol['children'] = array_merge($primaryCol['children'], $orphans); $this->logMessage("-> Normalizzazione: " . count($orphans) . " elementi orfani spostati dentro la colonna principale."); } $finalRows = [$primaryCol]; } } else if ($isBlockContext && !empty($finalRows)) { $collectedBlocks = []; $collectRecursive = function($items) use (&$collectRecursive, &$collectedBlocks) { foreach ($items as $item) { if (($item['type'] ?? '') === 'block') { $collectedBlocks[] = $item; } if (!empty($item['children']) && is_array($item['children'])) { $collectRecursive($item['children']); } } }; $collectRecursive($finalRows); $blockCount = count($collectedBlocks); if ($blockCount === 1) { $finalRows = [$collectedBlocks[0]]; $this->logMessage("-> Normalizzazione (Block): Singolo blocco estratto dalla struttura."); } else if ($blockCount > 1) { $this->logMessage("-> Normalizzazione (Block): $blockCount blocchi trovati. Mantengo wrapper ma rimuovo sfondi."); $cleanContainers = function (&$items) use (&$cleanContainers) { foreach ($items as &$item) { if (in_array(($item['type'] ?? ''), ['row', 'col'])) { if (isset($item['json']['design']['background'])) { $item['json']['design']['background'] = []; } if (isset($item['json']['design']['padding'])) { $item['json']['design']['padding'] = []; } if (!empty($item['children'])) { $cleanContainers($item['children']); } } } }; $cleanContainers($finalRows); } } // --- ASSEMBLAGGIO E GESTIONE IMMAGINI --- $wizard->setUpdates("{{Finalizzo}}", "{{Assemblo la pagina...}}", self::WIZARD_ICON); $assembledPage = $this->_assemblePage($finalRows); $finalRowsStructure = $assembledPage['rows']; $totalImagesToGenerate = 0; array_walk_recursive($finalRowsStructure, function($value, $key) use (&$totalImagesToGenerate) { if ($key === 'ai-query' && !empty($value)) { $totalImagesToGenerate++; } }); if ($totalImagesToGenerate > 0) { $wizard->setUpdateSteps(1, $totalImagesToGenerate); $wizard->setUpdates( "{{Generazione immagini}}", "{{Sto generando {$totalImagesToGenerate} nuove immagini...}}", self::WIZARD_ICON ); } $result = $this->_postProcessJson($finalRowsStructure, 'edit'); $costs = $this->getRealCost(); $result['meta_costs'] = $costs; $this->logMessage("COSTO EDITING: €" . $costs['total']); \Container::get('\MSFramework\Framework\credits')->removeCredits($costs['total'], "Modifica elemento tramite AI"); $title = mb_strimwidth(strip_tags($editPrompt), 0, 80, "..."); $this->saveToLibrary('edit_page', $title, json_encode($result)); return $result; } /** * NUOVO: Analizza l'intento dell'utente tramite AI per trasformare un prompt vago in uno specifico. * Questo previene che il "Regista" prenda decisioni distruttive basate su ambiguità. * * @param string $userPrompt Il prompt originale dell'utente (es. "Migliorala"). * @param array $sectionRows Il JSON della sezione/righe attualmente selezionate. * @param string $semanticContext Il contesto semantico dell'intera pagina. * @return array Un array contenente ['intent_analysis' => '...', 'refined_prompt' => '...'] o un fallback. */ private function _analyzeUserIntent(string $userPrompt, array $sectionRows, string $semanticContext): array { $this->logMessage("--- INIZIO ANALISI INTENTO UTENTE ---"); $this->logMessage("Prompt Originale: {$userPrompt}"); // Se il prompt è già specifico, non c'è bisogno di analizzarlo e di usare la cache. if (strlen($userPrompt) > 50 || str_word_count($userPrompt) > 5) { $this->logMessage("-> Prompt già specifico, analisi saltata."); return [ 'intent_analysis' => 'Il prompt utente è stato considerato già sufficientemente specifico.', 'refined_prompt' => $userPrompt, ]; } // ================================================================================= // INTEGRAZIONE SISTEMA CACHE // ================================================================================= // 1. Creiamo una chiave univoca basata su tutti gli input che influenzano il risultato. $cacheKey = 'intent_analysis_v2_' . md5(serialize([$userPrompt, $sectionRows, $semanticContext])); // 2. Controlliamo se esiste una risposta valida in cache. if ($cachedData = $this->checkCache($cacheKey)) { $this->logMessage("-> Analisi Intento recuperata da CACHE: {$cacheKey}"); return $cachedData; } // ================================================================================= $sectionJson = json_encode($sectionRows, JSON_PRETTY_PRINT); $systemPrompt = << Il `refined_prompt` DEVE essere: "Migliora i testi e il copywriting di questa sezione per renderli più accattivanti e professionali, mantenendo la struttura attuale." * Se il design è scarno (pochi colori, allineamenti base, sfondi bianchi) -> Il `refined_prompt` DEVE essere: "Migliora drasticamente il design e lo stile di questa sezione, applicando colori moderni, un layout più dinamico e sfondi interessanti, ma preservando i testi e la struttura dei blocchi." * Se mancano immagini (`single-image`, `gallery`) -> Il `refined_prompt` DEVE essere: "Aggiungi una o più immagini pertinenti per arricchire visivamente questa sezione, mantenendo i testi esistenti." 2. **Priorità alla Conservazione:** Il tuo `refined_prompt` deve SEMPRE includere la direttiva di **"mantenere la struttura attuale"** o **"preservare i contenuti esistenti"**, a meno che l'utente non chieda esplicitamente di "aggiungere" o "rimuovere" elementi. 3. **Sii Specifico:** Evita verbi generici come 'migliora' nel tuo output. Usa verbi d'azione: "Riscrivi i testi", "Cambia i colori", "Aggiungi un'immagine", "Modifica il layout in due colonne". Rispondi SOLO con il JSON valido. PROMPT; $userCommand = "Analizza la richiesta utente ('{$userPrompt}') e il JSON fornito, quindi genera il JSON con l'analisi e il prompt raffinato."; try { $config = ['temperature' => 0.1, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userCommand, $config); $result = json_decode($this->_cleanJsonResponse($response->text()), true); if (json_last_error() === JSON_ERROR_NONE && isset($result['refined_prompt'])) { $this->logMessage("-> Analisi completata. Prompt raffinato: " . $result['refined_prompt']); // 3. Salviamo il risultato corretto in cache per le prossime volte. $this->saveCache($cacheKey, $result); return $result; } else { throw new \Exception("L'AI per l'analisi dell'intento ha restituito un JSON non valido."); } } catch (\Exception $e) { $this->logMessage("-> ERRORE durante l'analisi dell'intento: " . $e->getMessage()); // Fallback: se l'analisi fallisce, si procede con il prompt originale. return [ 'intent_analysis' => 'Analisi fallita, si utilizza il prompt originale.', 'refined_prompt' => $userPrompt, ]; } } /** * IL REGISTA: Decide il piano d'azione e la STRATEGIA per ogni riga. * Analizza le keyword della struttura per capire a quale sezione si riferisce l'utente. */ private function _generateActionPlan($structureSummary, $userPrompt) { $structureJson = json_encode($structureSummary); $systemPrompt = << 0.1, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userPromptText, $config); $plan = json_decode($this->_cleanJsonResponse($response->text()), true); if (json_last_error() === JSON_ERROR_NONE && is_array($plan)) { return $plan; } } catch (\Exception $e) { $this->logMessage("Errore Director AI: " . $e->getMessage()); } // Fallback di sicurezza: se l'AI fallisce, manteniamo tutto invariato $fallback = []; foreach ($structureSummary as $item) { $fallback[] = ['action' => 'KEEP', 'original_index' => $item['index']]; } return $fallback; } private function _summarizePageStructure($rows) { $summary = []; foreach ($rows as $index => $row) { if (($row['type'] ?? '') === 'settings') continue; $info = [ 'index' => $index, 'type' => 'generic', 'keywords' => [] ]; $this->_analyzeRowFeatures($row, $info); // Limitiamo le keywords per ogni riga $info['keywords'] = array_slice(array_unique($info['keywords']), 0, 8); $summary[] = $info; } return $summary; } private function _analyzeRowFeatures($item, &$info) { if (isset($item['block_type'])) { $textFound = ''; // Caso 1: Titoli e Bottoni (json.data.text) if (isset($item['json']['data']['text']) && is_string($item['json']['data']['text'])) { $textFound = $item['json']['data']['text']; } // Caso 2: Text Editor (json.data.content) else if (isset($item['json']['data']['content']) && is_string($item['json']['data']['content'])) { $textFound = $item['json']['data']['content']; } if (!empty($textFound)) { $clean = trim(strip_tags($textFound)); // Ignoriamo testi troppo corti o Lorem Ipsum if (strlen($clean) > 3 && !str_contains(strtolower($clean), 'lorem')) { // Tronchiamo se troppo lungo if (strlen($clean) > 60) $clean = substr($clean, 0, 60) . '...'; $info['keywords'][] = $clean; } } } if (isset($item['children'])) { foreach ($item['children'] as $c) $this->_analyzeRowFeatures($c, $info); } } private function _extractVisualIdentityFromRows($rows) { // 1. RECUPERO CONFIGURAZIONE GLOBALE (Fonte di Verità) $themeSettings = \Container::get('\MSFramework\Frontend\parser')->themeSettings['colors']; // 2. COSTRUZIONE PALETTE COMPLETA (con Fallback) // Assicuriamo che tutte le chiavi semantiche abbiano un valore HEX valido $palette = [ 'primary' => $themeSettings['primary'] ?? '#000000', 'secondary' => $themeSettings['secondary'] ?? '#ffffff', 'success' => $themeSettings['success'] ?? '#28a745', 'danger' => $themeSettings['danger'] ?? '#dc3545', 'warning' => $themeSettings['warning'] ?? '#ffc107', 'info' => $themeSettings['info'] ?? '#17a2b8', 'light' => $themeSettings['light'] ?? '#f8f9fa', 'dark' => $themeSettings['dark'] ?? '#343a40', 'white' => $themeSettings['white'] ?? '#ffffff', 'black' => $themeSettings['black'] ?? '#000000', 'background'=> $themeSettings['background']?? '#ffffff' ]; // 3. RECUPERO TIPOGRAFIA GLOBALE $typography = [ 'headingFont' => $themeSettings['font_headers'] ?? 'Arial', 'bodyFont' => $themeSettings['font_body'] ?? 'Arial' ]; // 4. SCANSIONE RIGHE PER OVERRIDE LOCALI $walker = function($item) use (&$palette, &$typography, &$walker) { // A. Rilevamento Font (Spesso le landing hanno font diversi dal sito) if (isset($item['json']['design']['content']['font-family'])) { $font = $item['json']['design']['content']['font-family']; if (!empty($font) && $font !== 'inherit') { if ($typography['headingFont'] === 'Arial' || $typography['headingFont'] === 'inherit') { $typography['headingFont'] = $font; $typography['bodyFont'] = $font; } } } // B. Rilevamento Colori Impliciti dei Bottoni if (($item['block_type'] ?? '') === 'main/button') { $btnData = $item['json']['data']['main']['button'] ?? []; // Se c'è un colore esplicito custom, potremmo considerarlo come "Primary della pagina" if (isset($btnData['design']['background']['color'])) { $col = $btnData['design']['background']['color']; if ($col && $col !== 'transparent' && !array_key_exists($col, $palette)) { $btnType = array_key_exists($btnData['type'], $palette) ? $btnData['type'] : 'primary'; $palette[$btnType] = $col; } } } // C. Rilevamento Sfondo Pagina (Row Background) if (($item['type'] ?? '') === 'row') { if (isset($item['json']['design']['background']['color'])) { $bg = $item['json']['design']['background']['color']; // Se troviamo un colore di sfondo non trasparente, potrebbe essere il background della pagina if ($bg && $bg !== 'transparent' && $bg !== '#ffffff' && $bg !== '#FFFFFF') { $palette['background'] = $bg; } } } // Ricorsione if (isset($item['children'])) { foreach ($item['children'] as $c) $walker($c); } }; // Eseguiamo la scansione foreach ($rows as $row) $walker($row); return [ 'colorPalette' => $palette, 'typography' => $typography ]; } } ----- File: funnel.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\funnel.php ---------------------------------------- stepsData = $stepsData; $prompt = $stepsData['prompt']['value']; $targetLanguage = \Container::get('\MSFramework\i18n')->getCurrentLanguageDetails()['italian_name'] ?? 'italiano'; $wizard = \Container::get('\MSFramework\Framework\wizard'); // 1. Definisco l'identità visiva globale (se non esiste) $wizard->setUpdates("{{Definisco la strategia}}", "{{Creo l'identità visiva del brand...}}", self::WIZARD_ICON); $this->_initGlobalVisualIdentity($prompt, $targetLanguage); // 2. Genero il Blueprint (la struttura del funnel) $wizard->setUpdates("{{Disegno il flusso}}", "{{L'AI sta progettando l'architettura del funnel...}}", self::WIZARD_ICON); $blueprint = $this->_generateFunnelBlueprint($prompt, $targetLanguage); if (isset($blueprint['error'])) { return $blueprint; } $totalNodes = count($blueprint['nodes']); $wizard->setUpdateSteps(1, $totalNodes + 1); // +1 per l'assemblaggio finale $funnelData = [ "type" => "funnel", "title" => $blueprint['title'] ?? "Funnel AI Generated", "description" => $blueprint['description'] ?? $prompt, "content" => [ "id" => 0, // Sarà assegnato dal DB "autore" => 0, "nome" => $blueprint['title'], "type" => "funnel", "origin" => [], // Sarà popolato con il primo nodo "diagramma" => [], "data_creazione" => date('Y-m-d H:i:s'), "campaign_stats" => [ "Contacts" => 0, "Mail_Sent" => 0, "Sms_Sent" => 0, "Push_Sent" => 0, "Automation_End" => 0 ], "status" => 0 ] ]; // 3. Generazione dei singoli nodi foreach ($blueprint['nodes'] as $index => $node) { $nodeType = $node['type']; $nodeName = $node['name'] ?? $nodeType; $wizard->setUpdates("{{Creo: $nodeName}}", "{{Sto generando contenuti e configurazioni...}}", self::WIZARD_ICON); $generatedNode = null; if ($nodeType === 'page') { // Generazione Pagina Web (usa il generatore esistente) $generatedNode = $this->_generatePageNode($node, $targetLanguage); } else { // Generazione Email, Condizioni, etc. $generatedNode = $this->_generateGenericNode($node, $targetLanguage); } if ($generatedNode && !isset($generatedNode['error'])) { // Se è il nodo root/origin (di solito la prima pagina) if (isset($node['is_origin']) && $node['is_origin'] === true) { $funnelData['content']['origin']["0"] = $generatedNode; } else { $nodeId = $generatedNode['id'] ?? uniqid($nodeType . '_'); // Assicuro che l'ID sia nel formato chiave dell'array diagramma $funnelData['content']['diagramma'][$nodeId] = $generatedNode; } } else { $this->logMessage("Errore generazione nodo {$nodeName}: " . ($generatedNode['error'] ?? 'Errore sconosciuto')); } $wizard->setUpdateSteps('+1'); } // 4. Linkage: Ricostruisco le connessioni (parents) basandomi sul blueprint $this->_linkNodes($funnelData, $blueprint['connections']); $wizard->setUpdates("{{Finalizzo}}", "{{Sto salvando il funnel...}}", self::WIZARD_ICON); \Container::get('ErrorTracker')->console($funnelData, 'FunnelData'); // Generazione Titolo $userPrompt = $stepsData['prompt']['value'] ?? 'Nuovo Funnel'; $title = mb_strimwidth(strip_tags($userPrompt), 0, 80, "..."); // Salvataggio $this->saveToLibrary('funnel', $title, json_encode($funnelData)); return $funnelData; } /** * Restituisce la lista dei blocchi disponibili per il Blueprint * @return string */ private function getAvailableBlocks() { $availblocks = []; foreach(\Container::get('\MSFramework\Marketing\funnel')->getAllBtns() as $btnID => $btnValue) { $availblocks[] = '- ' . $btnID . ': ' . $btnValue['title'] . ' | ' . $btnValue['description']; } return implode(PHP_EOL, $availblocks); } /** * Restituisce l'esempio JSON per un tipo specifico di nodo * @param string $type * @return array */ private function getNodeExample($type) { $dummyFunnel = \Container::get('\MSFramework\AI\generator')->getDummyRow('marketing__campagne', 2); $exampleData = []; if($type === 'start_funnel') { $exampleData = json_decode(\Container::get('\MSFramework\AI\generator')->getDummyRow('marketing__tmp_actions', json_decode($dummyFunnel['origin'], true)[0]['action_id'])['content'], true); } else { foreach(json_decode($dummyFunnel['diagramma'], true) as $action) { if($action['type'] === $type) { if($action['data']['action_id']) { $exampleData = json_decode(\Container::get('\MSFramework\AI\generator')->getDummyRow('marketing__tmp_actions', $action['data']['action_id'])['content'], true); } else { $exampleData = $action['data']; } } } } return $exampleData; } /** * Fase 1: Genera o recupera l'identità visiva globale */ private function _initGlobalVisualIdentity($prompt, $targetLanguage) { $systemPrompt = $this->_buildStyleSystemPrompt($targetLanguage); $userPrompt = "Obiettivo Funnel: $prompt"; try { $config = ['temperature' => 0.7, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $jsonText = $this->_cleanJsonResponse($response->text()); $this->globalVisualIdentity = json_decode($jsonText, true); } catch (\Exception $e) { $this->logMessage("Errore generazione stile globale: " . $e->getMessage()); // Fallback minimale $this->globalVisualIdentity = ['visualIdentity' => ['colorPalette' => ['primary' => '#000000']]]; } } /** * Fase 2: Chiede all'AI di disegnare l'architettura del funnel */ private function _generateFunnelBlueprint($prompt, $targetLanguage) { $availableBlocks = $this->getAvailableBlocks(); $systemPrompt = << Email -> Thank You Page. Importante: Assicurati che un solo nodo abbia "is_origin": true. Lingua: {$targetLanguage} PROMPT; $userPrompt = "Crea un funnel per: " . $prompt; try { $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, ['jsonMode' => true]); return json_decode($this->_cleanJsonResponse($response->text()), true); } catch (\Exception $e) { return ['error' => 'Errore generazione blueprint: ' . $e->getMessage()]; } } /** * Fase 3a: Generazione Nodo Pagina (Delegato al Page Generator) */ private function _generatePageNode($nodeData, $targetLanguage) { // Prepariamo i dati per il generatore di pagine $pageGenerator = new \MSFramework\AI\generator\generator(); $pageGenerator->globalVisualIdentity = $this->globalVisualIdentity['visualIdentity'] ?? []; $pageGenerator->disableAutoSave = true; // Creiamo una struttura fittizia di "stepsData" per il generatore $fakeStepsData = $this->stepsData; $fakeStepsData['import-prompt']['value'] = $nodeData['purpose']; // Chiamiamo il metodo principale di generazione //$pageResult = $pageGenerator->generatePage($fakeStepsData); $pageResult = []; if (isset($pageResult['error'])) { return ['error' => $pageResult['error']]; } // Costruiamo il nodo nel formato Funnel return [ "id" => $nodeData['id'], "type" => "page", "parent" => 0, // Sarà impostato in fase di Linkage "data" => [ "title" => ["it_IT" => $nodeData['name']], "slug" => ["it_IT" => \Container::get('\MSFramework\url')->cleanString($nodeData['name'] . '-' . uniqid())], "content" => json_encode($pageResult), // Il JSON del visual builder "seo" => [ "seo" => [ "title" => ["it_IT" => $nodeData['name']], "description" => ["it_IT" => $nodeData['purpose']], "indexing" => "index" ] ], "id" => "", "need_auth" => null, "action_id" => rand(100000, 999999) ] ]; } /** * Fase 3b: Generazione Nodi Generici (Email, Condition, etc.) */ private function _generateGenericNode($nodeData, $targetLanguage) { $type = $nodeData['type']; $typeData = \Container::get('\MSFramework\Marketing\funnel')->getAllBtns()[$type]; // Recupero l'esempio specifico tramite funzione $exampleData = $this->getNodeExample($type); if (empty($exampleData)) { // Se non ho esempi, ritorno un errore o un fallback vuoto per evitare crash return ['error' => "Nessun template trovato per il tipo: $type"]; } $examplesJson = json_encode($exampleData, JSON_PRETTY_PRINT); $visualCtx = json_encode($this->globalVisualIdentity['visualIdentity'] ?? []); $typeSuggestion = ''; if($typeData && $typeData['ai'] && $typeData['ai']['suggestion']) { $typeSuggestion = "** INFORMAZIONI AGGIUNTIVE:***" . PHP_EOL . $typeData['ai']['suggestion']; } $systemPrompt = <<aiClient->generateContent($systemPrompt, $userPrompt, ['jsonMode' => true]); $dataContent = json_decode($this->_cleanJsonResponse($response->text()), true); if($typeData && $typeData['ai'] && $typeData['ai']['prepare']) { $dataContent = $typeData['ai']['prepare']($dataContent); } return [ "id" => $nodeData['id'], "type" => $type, "parent" => 0, // Linkage dopo "data" => $dataContent ]; } catch (\Exception $e) { $this->logMessage("Errore nodo generico $type: " . $e->getMessage()); return ['error' => $e->getMessage()]; } } /** * Fase 4: Linkage (Collega i nodi impostando il campo 'parent') */ private function _linkNodes(&$funnelData, $connections) { // Mappa ID AI (es. step_1) -> ID Reale (chiave array diagramma) // Nota: Nel loop precedente abbiamo usato $node['id'] come chiave dell'array diagramma, quindi corrisponde. foreach ($connections as $conn) { $fromId = $conn['from']; $toId = $conn['to']; // Cerco il nodo destinazione in diagramma if (isset($funnelData['content']['diagramma'][$toId])) { // Imposto il parent // In MSFramework funnel, 'parent' è l'ID del nodo che punta a me $parentVal = $fromId; // Gestione casi speciali (es. uscita da condizione) if (isset($conn['condition_result'])) { // Se vengo da una condizione, l'ID parent deve avere il suffisso del risultato // Es: condition_id_result_1 (True) o condition_id_result_0 (False) $parentVal = $fromId . '_result_' . $conn['condition_result']; } $funnelData['content']['diagramma'][$toId]['parent'] = $parentVal; } // Nota: Se il nodo 'from' è l'origin (chiave "0" o ID fisso), il linkage è implicito // o gestito separatamente se il diagramma inizia con un parent specifico. // Qui assumiamo che i nodi generati in diagramma abbiano parent settati correttamente dalle connessioni AI. } } private function _buildStyleSystemPrompt($targetLanguage) { // Copia ridotta del prompt presente in generator.php per definire colori e font return "Sei un Art Director. Genera un JSON con 'visualIdentity' contenente 'colorPalette' (primary, secondary hex), 'typography' (font family) e 'designStyle'. Usa colori professionali."; } } ----- File: generator.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\generator.php ---------------------------------------- stepsData = $stepsData; $prompt = $stepsData['import-prompt']['value']; set_time_limit(900); $jobId = md5('gemini_v15_generate_' . trim(strtolower($prompt))); $this->qualityMetrics = ['sections_generated' => 0, 'sections_failed' => 0, 'refinement_cycles' => 0, 'validation_passes' => 0, 'validation_failures' => 0]; $targetLanguage = \Container::get('\MSFramework\i18n')->getCurrentLanguageDetails()['italian_name'] ?? 'italiano'; $this->totalTokensUsed = 0; $this->logMessage("=== NUOVA ESECUZIONE GENERATEPAGE (v15 MIGLIORATA) ==="); $this->logMessage("Job ID: {$jobId}"); $wizard = \Container::get('\MSFramework\Framework\wizard'); $wizard->setUpdates("{{Inizio il processo creativo}}", "{{Sto analizzando la tua richiesta...}}", self::WIZARD_ICON); $this->_loadReferenceFiles(); $jobCacheDir = null; if ($this->useIncrementalCache) { $jobCacheDir = $this->cacheDir . '/' . $jobId; if (!is_dir($jobCacheDir)) mkdir($jobCacheDir, 0775, true); $this->logMessage("Cache incrementale: {$jobCacheDir}"); } $wizard->setUpdates("{{Definisco la tua identità visiva}}", "{{Creo la personalità unica del tuo brand...}}", self::WIZARD_ICON); $guideCacheFile = $jobCacheDir ? $jobCacheDir . '/guide.json' : null; if ($guideCacheFile && file_exists($guideCacheFile)) { $pageGuide = json_decode(file_get_contents($guideCacheFile), true); $this->logMessage("Guida recuperata da cache"); } else { $pageGuide = $this->_generatePageGuide($prompt, $targetLanguage); $this->logMessage("GUIDA DI STILE:\n" . json_encode($pageGuide, JSON_PRETTY_PRINT)); if ($guideCacheFile && !isset($pageGuide['error'])) { file_put_contents($guideCacheFile, json_encode($pageGuide, JSON_PRETTY_PRINT)); } } // Aggiungo le voci di menù $this->menuVoices = [ 'voice_home' => [ 'title' => 'Home', 'target' => '_self', 'url' => '#', 'show_in_menu' => true ] ]; // Creiamo una lista semplice di ID e Scopi da passare a tutte le sezioni if (isset($pageGuide['pageArchitecture']) && is_array($pageGuide['pageArchitecture'])) { foreach ($pageGuide['pageArchitecture'] as $archItem) { if(!$archItem['menu_id']) continue; $this->menuVoices['voice_' . ltrim($archItem['menu_id'], '#')] = [ 'title' => $archItem['menu_title'] ?: $archItem['type'], 'target' => '_self', 'url' => '#' . ltrim($archItem['menu_id'], '#'), 'show_in_menu' => $archItem['show_in_menu'] ]; } } if (isset($pageGuide['error']) || empty($pageGuide['visualIdentity']) || empty($pageGuide['pageArchitecture'])) { $errorMsg = $pageGuide['error'] ?? 'Guida non valida'; $this->logMessage("ERRORE FASE 2: {$errorMsg}"); return ['error' => 'Non sono riuscito a definire una guida di stile. Riprova.']; } $totalSections = count($pageGuide['pageArchitecture']); $wizard->setUpdateSteps(1, $totalSections); $wizard->setUpdateSteps('+1'); $this->logMessage("FASE 3 - Generazione {$totalSections} sezioni\n"); $generatedRows = []; $this->sectionContext = [ 'previousSections' => [], 'totalSections' => $totalSections, 'visualIdentity' => $pageGuide['visualIdentity'] ]; if ($cachedData = $this->checkCache($jobId)) { $this->logMessage("RECUPERATO DA CACHE: Job ID {$jobId}"); $cachedData = array_map(function($r) { return $this->_autoCorrectSection($r); }, $cachedData); $this->disableAutoSave = true; //return $this->_postProcessJson($cachedData, 'generate'); } foreach (array_values($pageGuide['pageArchitecture']) as $index => $sectionData) { $sectionNumber = $index + 1; $sectionFriendlyName = $sectionData['menu_title'] ?: $this->getSectionFriendlyName($sectionData['type']); $wizard->setUpdates("{{Sezione}} " . strtolower($sectionFriendlyName), "{{Sto costruendo la sezione}} " . strtolower($sectionFriendlyName) . "...", self::WIZARD_ICON); $this->logMessage("SEZIONE {$sectionNumber}/{$totalSections}: {$sectionFriendlyName}"); $latestSectionJson = null; $sectionCacheFile = $jobCacheDir ? $jobCacheDir . '/section_' . $index . '.json' : null; if ($sectionCacheFile && file_exists($sectionCacheFile)) { $latestSectionJson = json_decode(file_get_contents($sectionCacheFile), true); $this->logMessage(" → Da cache"); } else { $sectionRows = $this->_generateSectionWithRetry( $sectionData, $pageGuide['visualIdentity'], $targetLanguage, $wizard, $sectionNumber, $totalSections, $prompt, null ); if (isset($sectionRows['error'])) { $this->logMessage(" → ERRORE: {$sectionRows['error']}"); $this->qualityMetrics['sections_failed']++; continue; } $latestSectionJson = $sectionRows; if ($sectionCacheFile) { file_put_contents($sectionCacheFile, json_encode($latestSectionJson, JSON_PRETTY_PRINT)); } } if (!empty($latestSectionJson)) { if ($sectionData['type'] === 'header') { $this->logMessage(" → Ottimizzazione strutturale Header in corso..."); $latestSectionJson = $this->_optimizeHeaderStructure($latestSectionJson); } if ($sectionData['type'] === 'footer') { $this->logMessage(" → Ottimizzazione strutturale Footer in corso..."); $latestSectionJson = $this->_optimizeFooterStructure($latestSectionJson); } if($sectionData) { if(!isset($latestSectionJson[0]['json']['design']['attributes'])) $latestSectionJson[0]['json']['design']['attributes'] = []; $latestSectionJson[0]['json']['design']['attributes']['id'] = ltrim($sectionData['menu_id'], '#'); } foreach ($latestSectionJson as $row) { $generatedRows[] = $row; } $this->qualityMetrics['sections_generated']++; $this->sectionContext['previousSections'][] = [ 'type' => $sectionData['type'], 'summary' => $this->_summarizeSection($latestSectionJson) ]; } $wizard->setUpdateSteps('+1'); } if (empty($generatedRows)) { $errorMsg = "Nessuna sezione generata con successo."; $this->logMessage("ERRORE FINALE: {$errorMsg}"); return ['error' => $errorMsg]; } $wizard->setUpdates("{{Finalizzo}}", "{{Sto assemblando la pagina e applicando gli ultimi ritocchi...}}", self::WIZARD_ICON); $finalJson = $this->_assemblePage($generatedRows); $finalJson = $this->_injectMenuIntoStructure($finalJson); $this->saveCache($jobId, $finalJson); // 1. Contiamo quante immagini l'AI ha richiesto (chiavi 'ai-query') $totalImagesToGenerate = 0; array_walk_recursive($finalJson, function($value, $key) use (&$totalImagesToGenerate) { // Contiamo solo se c'è una query valida if ($key === 'ai-query' && !empty($value)) { $totalImagesToGenerate++; } }); // 2. Se ci sono immagini, aggiorniamo il totale degli step del Wizard if ($totalImagesToGenerate > 0) { // Aggiorniamo il wizard con il nuovo totale $wizard->setUpdateSteps(1, $totalImagesToGenerate); // Informiamo l'utente $wizard->setUpdates( "{{Generazione immagini}}", "{{Sto generando {$totalImagesToGenerate} immagini uniche per il tuo design...}}", self::WIZARD_ICON ); } else { $wizard->setUpdates("{{Finalizzo}}", "{{Sto assemblando la pagina e applicando gli ultimi ritocchi...}}", self::WIZARD_ICON); } $this->logMessage("METRICHE QUALITÀ: " . json_encode($this->qualityMetrics, JSON_PRETTY_PRINT)); $result = $this->_postProcessJson($finalJson, 'generate'); $costs = $this->getRealCost(); $result['meta_costs'] = $costs; $this->logMessage("COSTO GENERAZIONE: €" . $costs['total']); \Container::get('\MSFramework\Framework\credits')->removeCredits($costs['total'], "Generazione pagina tramite AI"); if (!$this->disableAutoSave) { // Generazione Titolo: Tronca il prompt utente o usa un default $userPrompt = $stepsData['import-prompt']['value'] ?? 'Nuova Pagina'; $title = mb_strimwidth(strip_tags($userPrompt), 0, 80, "..."); // Salvataggio $this->saveToLibrary('generate_page', $title, json_encode($result)); } return $result; } /** * Cerca ricorsivamente il blocco menù e inietta le voci calcolate */ private function _injectMenuIntoStructure($rows) { return \Container::get('\MSFramework\utils')->array_map_recursive(function($item) { if (!is_array($item)) return $item; if (isset($item['block_type']) && $item['block_type'] === 'main/menu') { $item['json']['data']['source_mode'] = 'manual'; $item['json']['data']['manual_items'] = array_filter($this->menuVoices, function($voice) { return $voice['show_in_menu']; }); $this->logMessage("Menù aggiornato con " . count($this->menuVoices) . " voci: " . json_encode($this->menuVoices)); } return $item; }, $rows, true); } private function _generatePageGuide($prompt, $targetLanguage) { $systemPrompt = $this->_buildPageGuideSystemPrompt($targetLanguage); $userPrompt = $this->_buildPageGuideUserPrompt($prompt); $this->logMessage("--- PROMPT GENERAZIONE GUIDA ---"); $this->logMessage("SYSTEM:"); $this->logMessage($systemPrompt); $this->logMessage("\nUSER:"); $this->logMessage($userPrompt); $this->logMessage("--- FINE PROMPT ---"); try { $config = ['temperature' => 0.8, 'topP' => 0.95, 'topK' => 40, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $this->logMessage("RISPOSTA AI:"); $this->logMessage($response->text()); $jsonText = $this->_cleanJsonResponse($response->text()); $pageGuide = json_decode($jsonText, true); if (json_last_error() !== JSON_ERROR_NONE) { return ['error' => 'Errore parsing JSON guida: ' . json_last_error_msg()]; } if($this->globalVisualIdentity) { $pageGuide['visualIdentity'] = $this->globalVisualIdentity; } return $pageGuide; } catch (\Exception $e) { $this->logMessage("ERRORE API GEMINI (guida): " . $e->getMessage()); return ['error' => 'Errore comunicazione con AI: ' . $e->getMessage()]; } } protected function _generateSectionWithRetry($sectionData, $visualIdentity, $targetLanguage, $wizard, $sectionNumber, $totalSections, $globalContext = '', $existingRowJson = null) { $maxAttempts = self::MAX_REFINEMENT_ATTEMPTS; // Temperatura adattiva: più alta in edit per favorire creatività (es. testi) $temperature = ($existingRowJson) ? 0.35 : 0.9; // Selezioniamo esempi. $bestExamples = $this->_selectBestExamples($sectionData['type'], $sectionData['purpose'] ?? ''); $examplesText = $this->_formatExamplesForPrompt($bestExamples); for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { if ($attempt > 1) { $randomMsg = self::POSITIVE_CORRECTION_MESSAGES[array_rand(self::POSITIVE_CORRECTION_MESSAGES)]; $wizard->setUpdates($randomMsg, "{{Tento una correzione...}}", self::WIZARD_ICON); } // Prepariamo i flag per il prompt builder $isEditMode = !empty($existingRowJson); $systemPrompt = $this->_buildSectionSystemPrompt($sectionData, $visualIdentity, $targetLanguage, $examplesText, $isEditMode); $userPrompt = $this->_buildSectionUserPrompt($sectionData, $sectionNumber, $totalSections, $globalContext, $existingRowJson); try { $config = ['temperature' => $temperature, 'topP' => 0.95, 'topK' => 50, 'jsonMode' => true]; $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config); $jsonText = $this->_cleanJsonResponse($response->text()); $generatedData = json_decode($jsonText, true); if (json_last_error() !== JSON_ERROR_NONE) { continue; // Riprova se il JSON è rotto } // Normalizzazione Array: Assicuriamoci di avere un array di oggetti if (isset($generatedData['type']) || isset($generatedData['block_type'])) { $generatedData = [$generatedData]; } // ----------------------------------------------------------- // VALIDAZIONE // ----------------------------------------------------------- // Eseguiamo una validazione generica per assicurarci che non ci siano allucinazioni gravi $validationResult = $this->_validateGeneratedSection($generatedData); if ($validationResult['valid']) { return $generatedData; } else { // Se siamo all'ultimo tentativo e abbiamo qualcosa, proviamo a correggerlo automaticamente if ($attempt === $maxAttempts && !empty($generatedData)) { return $this->_autoCorrectSection($generatedData); } // Altrimenti logghiamo e riproviamo $this->logMessage("Tentativo $attempt fallito validazione: " . implode(', ', $validationResult['errors'])); } } catch (\Exception $e) { $this->logMessage("ERRORE API GEMINI (sezione): " . $e->getMessage()); } $this->qualityMetrics['refinement_cycles']++; } return ['error' => 'Impossibile generare la sezione in modo valido dopo ' . $maxAttempts . ' tentativi']; } /** * Forza la struttura dell'header per seguire le best practices responsive. * Gestisce due scenari: * 1. Logo + Menu + CTA (3 colonne) -> Tablet: Logo(4)+Menu(8) | CTA(12) * 2. Logo + Menu (2 colonne) -> Tablet: Logo(4)+Menu(8) */ private function _optimizeHeaderStructure($rows) { foreach ($rows as &$row) { if (($row['type'] ?? '') !== 'row' || empty($row['children'])) continue; // 1. Mappatura colonne $logoColIndex = -1; $menuColIndex = -1; $ctaColIndex = -1; foreach ($row['children'] as $idx => $col) { if (empty($col['children'])) continue; foreach ($col['children'] as $block) { $bType = $block['block_type'] ?? ($block['json']['data']['type'] ?? ''); if ($bType === 'main/logo' || $bType === 'main/title') { $logoColIndex = $idx; } elseif ($bType === 'main/menu' || $bType === 'main/sitemap') { $menuColIndex = $idx; } elseif ($bType === 'main/button') { $ctaColIndex = $idx; } } } // 2. Applicazione Logica Scenario A: LOGO + MENU + CTA if ($logoColIndex !== -1 && $menuColIndex !== -1 && $ctaColIndex !== -1) { // A. COLONNA LOGO (Desktop: 3, Tablet: 4, Mobile: 8) $row['children'][$logoColIndex]['col'] = 3; // Fallback $row['children'][$logoColIndex]['json']['responsive'] = [ 'desktop' => ['col' => 3], 'tablet' => ['col' => 4], 'mobile' => ['col' => 8] ]; // Allineamento Mobile Logo: Sinistra if (!isset($row['children'][$logoColIndex]['json']['design']['mobile']['content'])) { $row['children'][$logoColIndex]['json']['design']['mobile']['content'] = []; } $row['children'][$logoColIndex]['json']['design']['mobile']['content']['align'] = 'flex-start'; // B. COLONNA MENU (Desktop: 6, Tablet: 8, Mobile: 4) $row['children'][$menuColIndex]['col'] = 6; $row['children'][$menuColIndex]['json']['responsive'] = [ 'desktop' => ['col' => 6], 'tablet' => ['col' => 8], 'mobile' => ['col' => 4] ]; // Configurazione Blocco Menu (Centrato su Desktop, Burger a Destra su Tablet/Mobile) foreach ($row['children'][$menuColIndex]['children'] as &$block) { $bType = $block['block_type'] ?? ''; if ($bType === 'main/menu') { // 1. Forza il breakpoint tablet per il burger menu if (!isset($block['json']['data']['button'])) $block['json']['data']['button'] = []; $block['json']['data']['button']['breakpoint'] = 'tablet'; // 2. FIX: Forza Allineamento CENTRALE su Desktop (visto che siamo nel layout a 3 colonne) if (!isset($block['json']['design']['content'])) $block['json']['design']['content'] = []; $block['json']['design']['content']['align'] = 'center'; // 3. Allineamento DX su Tablet e Mobile (per il burger menu) $alignRight = ['content' => ['align' => 'right']]; $block['json']['design']['mobile'] = array_replace_recursive($block['json']['design']['mobile'] ?? [], $alignRight); $block['json']['design']['tablet'] = array_replace_recursive($block['json']['design']['tablet'] ?? [], $alignRight); } } // C. COLONNA CTA (Desktop: 3, Tablet: 12, Mobile: 12) -> Va a capo su tablet $row['children'][$ctaColIndex]['col'] = 3; $row['children'][$ctaColIndex]['json']['responsive'] = [ 'desktop' => ['col' => 3], 'tablet' => ['col' => 12], 'mobile' => ['col' => 12] ]; // Configurazione Blocco Button (Full Width su Tablet) foreach ($row['children'][$ctaColIndex]['children'] as &$block) { $bType = $block['block_type'] ?? ''; if ($bType === 'main/button') { $block['json']['data']['main']['button']['alignment'] = ''; // Imposto gli allineamenti $block['json']['design']['content']['align'] = ''; $block['json']['design']['tablet']['content']['align'] = ''; $block['json']['data']['main']['button']['design']['content']['align'] = 'right'; $block['json']['data']['main']['button']['design']['tablet']['content']['align'] = 'center'; // Imposto le larghezze $block['json']['data']['main']['button']['design']['sizes'] = ['width' => '']; $block['json']['data']['main']['button']['design']['tablet']['sizes'] = ['width' => '100%']; } } // D. GESTIONE SPAZIO VERTICALE ROW (Padding extra su tablet per separare le righe) if (!isset($row['json']['design']['tablet']['content'])) { $row['json']['design']['tablet']['content'] = []; } $row['json']['design']['content']['col-spacing'] = '15'; } // 3. Applicazione Logica Scenario B: SOLO LOGO + MENU elseif ($logoColIndex !== -1 && $menuColIndex !== -1) { // A. COLONNA LOGO (Desktop: 3, Tablet: 4, Mobile: 8) $row['children'][$logoColIndex]['col'] = 3; $row['children'][$logoColIndex]['json']['responsive'] = [ 'desktop' => ['col' => 3], 'tablet' => ['col' => 4], 'mobile' => ['col' => 8] ]; if (!isset($row['children'][$logoColIndex]['json']['design']['mobile']['content'])) { $row['children'][$logoColIndex]['json']['design']['mobile']['content'] = []; } $row['children'][$logoColIndex]['json']['design']['mobile']['content']['align'] = 'flex-start'; // B. COLONNA MENU (Desktop: 9, Tablet: 8, Mobile: 4) $row['children'][$menuColIndex]['col'] = 9; $row['children'][$menuColIndex]['json']['responsive'] = [ 'desktop' => ['col' => 9], 'tablet' => ['col' => 8], 'mobile' => ['col' => 4] ]; // Configurazione Blocco Menu foreach ($row['children'][$menuColIndex]['children'] as &$block) { $bType = $block['block_type'] ?? ''; if ($bType === 'main/menu') { if (!isset($block['json']['data']['button'])) $block['json']['data']['button'] = []; $block['json']['data']['button']['breakpoint'] = 'tablet'; // Burger anche su tablet per sicurezza layout $alignRight = ['content' => ['align' => 'right']]; $block['json']['design']['mobile'] = array_replace_recursive($block['json']['design']['mobile'] ?? [], $alignRight); $block['json']['design']['tablet'] = array_replace_recursive($block['json']['design']['tablet'] ?? [], $alignRight); } } } if(count($this->menuVoices) > 5) { $row['json']['data']['width'] = 'full-width-and-content'; } } return $rows; } private function _optimizeFooterStructure($rows) { foreach ($rows as &$row) { if (($row['type'] ?? '') !== 'row' || empty($row['children'])) continue; // Iteriamo tutte le colonne del footer foreach ($row['children'] as &$col) { if (empty($col['children'])) continue; // Iteriamo i blocchi nella colonna foreach ($col['children'] as &$block) { $bType = $block['block_type'] ?? ($block['json']['data']['type'] ?? ''); // Intercettiamo il Menu if ($bType === 'main/menu') { // 1. Forza tema verticale if (!isset($block['json']['data'])) $block['json']['data'] = []; $block['json']['data']['theme'] = 'vertical'; // 2. Ottimizzazione Design: if (!isset($block['json']['design']['content'])) $block['json']['design']['content'] = []; if (empty($block['json']['design']['content']['align']) || $block['json']['design']['content']['align'] === 'center') { $block['json']['design']['content']['align'] = 'flex-start'; } $block['json']['design']['content']['line-height'] = ['value' => 1, 'unit' => 'em']; } } } } return $rows; } private function _buildPageGuideSystemPrompt($targetLanguage) { $blocksReferenceText = ''; if ($this->blocksReference) { $blocksReferenceText = "\n\n--- FILE DI RIFERIMENTO: fw360-importer-visualbuilder-blocks.json ---\n"; $blocksReferenceText .= json_encode($this->blocksReference, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $blocksReferenceText .= "\n--- FINE FILE DI RIFERIMENTO ---\n"; } $examplesText = ''; if (!empty($this->examplesContent)) { $examplesText = $this->examplesContent; } $colorsText = ''; $useColorNames = false; if($this->stepsData['colors']['value']) { if($this->stepsData['colors']['value'] === 'default') { $colorsText = "Usa i nomi semantici come valori (primary, secondary, success, danger, warning, info, light, dark, white, black). NON usare valori hex. Scegli i nomi appropriati in base al contesto."; $useColorNames = true; } else if($this->stepsData['colors']['value'] === 'custom') { $colorsText = "Usa " . $this->stepsData['colors']['color-1'] . " come primary e " . $this->stepsData['colors']['color-2'] . " come secondary. Genera gli altri colori in hex adatti e coordinati. NON usare mai i nomi semantici (primary, secondary, etc.) come valori - usa SOLO codici hex."; $useColorNames = false; } else if($this->stepsData['colors']['value'] !== 'ai') { $colors = explode('-', $this->stepsData['colors']['value']); $colorsText = "Usa tonalità di " . $colors[0] . " come primary e " . $colors[1] . " come secondary. Genera tutti i colori in formato hex. NON usare mai i nomi semantici (primary, secondary, etc.) come valori - usa SOLO codici hex."; $useColorNames = false; } } if(!$colorsText) { $colorsText = "Genera dei colori adatti in formato hex. NON usare mai i nomi semantici come valori."; $useColorNames = false; } if($useColorNames) { $colorFormatInstructions = "\n\n**FORMATO COLORI (CRITICO)**:\nPer questa pagina, devi usare i NOMI SEMANTICI come valori:\n- primary (non #hex)\n- secondary (non #hex)\n- success (non #hex)\n- danger (non #hex)\n- warning (non #hex)\n- info (non #hex)\n- light (non #hex)\n- dark (non #hex)\n- white (non #hex)\n- black (non #hex)\n\nEsempio CORRETTO: \"background\": { \"color\": \"primary\" }\nEsempio SBAGLIATO: \"background\": { \"color\": \"#1a73e8\" }\n"; } else { $colorFormatInstructions = "\n\n**FORMATO COLORI (CRITICO)**:\nPer questa pagina, devi usare SEMPRE e SOLO codici HEX come valori:\n- Usa #hex (es: #1a73e8, #ff6b35, #f8f9fa)\n- NON usare MAI i nomi semantici (primary, secondary, success, etc.) come valori\n- I nomi semantici sono SOLO chiavi nella palette, NON valori da usare nel design\n\nEsempio CORRETTO: \"background\": { \"color\": \"#1a73e8\" }\nEsempio SBAGLIATO: \"background\": { \"color\": \"primary\" }\n"; } return <<blocksReference) { $blocksReferenceText = "\n\n--- FILE DI RIFERIMENTO BLOCCHI (chiavi JSON valide) ---\n"; $blocksReferenceText .= json_encode($this->blocksReference, JSON_PRETTY_PRINT); $blocksReferenceText .= "\n--- FINE FILE DI RIFERIMENTO ---\n"; } $contextText = ''; if ($this->sectionContext && !empty($this->sectionContext['previousSections'])) { $contextText = "\n\n**CONTESTO SEZIONI PRECEDENTI**:\n"; foreach ($this->sectionContext['previousSections'] as $prev) { $contextText .= "- {$prev['type']}: " . json_encode($prev['summary']) . "\n"; } $contextText .= "\nUsa questo contesto per creare continuità e varietà. Non ripetere esattamente le stesse soluzioni visive.\n"; } $navigationMapText = ""; if ($this->menuVoices) { $navigationMapText = "\n\n**MAPPA DI NAVIGAZIONE INTERNA (ANCHOR LINKS):**\n"; $navigationMapText .= "La pagina contiene ESATTAMENTE le seguenti sezioni. Usa questi ID per i pulsanti (CTA) invece di link generici '#':\n"; foreach ($this->menuVoices as $sec) { if('#' . ltrim($sectionData['menu_id'], '#') === $sec['url']) { continue; } $cleanLabel = $sec['title']; $cleanUrl = $sec['url']; $navigationMapText .= "- Per mandare l'utente a '{$cleanLabel}' -> usa url: \"{$cleanUrl}\"\n"; } $navigationMapText .= "\n**REGOLA LINKING**:\n"; $navigationMapText .= "1. Se la CTA deve portare a un'azione interna (es. 'Contattaci', 'Scopri Prezzi'), DEVI usare uno degli ID sopra.\n"; $navigationMapText .= "2. Se la CTA è generica (es. 'Torna su'), usa \"#\".\n"; $navigationMapText .= "3. Se la CTA deve portare fuori (es. Facebook, Instagram), lascia \"#\" o metti un placeholder esplicito.\n"; } // --- GESTIONE LOGICA CONDIZIONALE IMMAGINI (GENERAZIONE vs MODIFICA) --- if ($isEditMode) { // REGOLE PER MODIFICA (Preservative) $imageRules = << 0.1) ❌ Gradienti con colori incompatibili ❌ Background scuro senza testo chiaro ❌ Immagini di sfondo senza overlay (testo illeggibile) ❌ Troppe immagini di sfondo (max 2-3 per pagina) **RICORDA**: Il background non è "decorazione opzionale" - è parte fondamentale del design. Ogni sezione deve avere un background intenzionale che: 1. Crea gerarchia visiva (quale sezione è più importante?) 2. Guida l'occhio (alterna chiaro/scuro per ritmo) 3. Supporta il brand (usa i colori della palette) 4. Migliora leggibilità (contrasto con il testo) 5. Aggiunge emozione (immagini per sezioni chiave) **SISTEMA DI SPAZIATURA E LAYOUT (REGOLE TECNICHE)**: 1. **DEFAULTS DI SISTEMA**: - Ogni riga ha nativamente **15px padding top/bottom**. Tienilo a mente quando calcoli lo spazio extra. - Ogni colonna ha nativamente **15px padding laterale**. 2. **GESTIONE GAP TRA COLONNE (BEST PRACTICE)**: - Per separare le colonne, NON usare margin sulle colonne. - USA `col-spacing` sulla RIGA genitore. - Esempio JSON riga: `"design": { "content": { "col-spacing": "30" } }` - Questo applicherà il gap in tutte le direzioni (sopra, sotto, destra, sinistra) in modo uniforme. 3. **SPACING E RITMO**: - Padding generoso (60-100px) per sezioni importanti, tight (20-40px) per dense - Margin tra elementi: usa multipli di 8px o 16px per coerenza (16, 24, 32, 48px) - Prediligi 'em' o 'rem' per spacing quando possibile per scalabilità - Crea "respiro" attorno elementi importanti - Non aver paura dello spazio negativo - è potente **FORMATTAZIONE TESTO (SENTENCE CASE)**: - **REGOLA CRITICA**: Scrivi i titoli in modo naturale ("Sentence case"). - USA: "Più di un semplice bar: i nostri pilastri di qualità" - EVITA: "Più Di Un Semplice Bar: I Nostri Pilastri Di Qualità" (Title Case) - Usa le maiuscole SOLO per: La prima lettera della frase, nomi propri, acronimi. **DESIGN RESPONSIVE (CRITICO)**: - Font size: usa valori che scalano bene su mobile * Titoli h1: 48-72px desktop → 32-40px mobile (riduci del 30-40%) * Titoli h2: 36-48px desktop → 24-32px mobile * Body text: 16-18px desktop → 14-16px mobile * Specifica SEMPRE entrambe le dimensioni quando rilevante - Spacing responsive: * Padding sezioni: 80-120px desktop → 40-60px mobile (riduci del 50%) * Margin tra elementi: 40-60px desktop → 20-30px mobile * Button padding: 16px 40px desktop → 12px 24px mobile - Layout mobile: * Le colonne si impilano automaticamente su mobile (diventa 100% width) * Considera l'ordine: l'elemento più importante deve apparire per primo su mobile * Immagini: assicurati che abbiano senso anche a piena larghezza mobile * Testi: mantieni leggibili, mai sotto 14px su mobile **ELEMENTI SPECIALI** (quando servono davvero): - Forme decorative (cerchi, linee sottili, blob organici) per personalità brand - Icone per comunicazione rapida e universale - Pattern di background a bassa opacità (0.03-0.05) per texture subtile - Immagini possono essere: rettangolari, circolari, con border-radius custom, con mask creative, con overlay colorato - Shadow per profondità: subtle (0 2px 4px), medium (0 4px 12px), strong (0 8px 24px) **REGOLE TECNICHE CRITICHE E INFRANGIBILI**: 1. **OUTPUT FORMATO**: Restituisci un array JSON contenente UNA O PIÙ RIGHE per questa sezione. Formato esatto: `[ { "type": "row", ... }, { "type": "row", ... } ]` 2. **STRUTTURA OBBLIGATORIA**: Ogni riga DEVE seguire questa gerarchia: - `row` (contenitore principale) - `children` (array di colonne `col`) - `children` (array di blocchi `block`) 3. **CHIAVI VALIDE**: Usa ESCLUSIVAMENTE chiavi presenti nel file di riferimento blocchi fornito sotto. NON inventare nuove chiavi JSON. NON modificare la struttura dei blocchi. Ogni block_type ha una struttura json.data specifica - rispettala esattamente. {$imageRules} 6. **COERENZA VISIVA**: Rispetta l'identità visiva fornita: - Usa SOLO i colori dalla colorPalette (o loro varianti con opacity) - Usa SOLO i font specificati in typography - Rispetta designStyle, spacing, cornerRadius, shadows come linee guida - Prediligi line-height in 'em' invece che 'px' 7. **LINGUA**: Tutti i testi visibili all'utente devono essere in {$targetLanguage} 8. **VALIDITÀ JSON**: La tua risposta deve essere SOLO JSON valido, parseable, senza: - Testo prima o dopo il JSON - Commenti dentro il JSON - Caratteri di escape non necessari - Strutture incomplete **SISTEMA COLONNE**: - Ogni row ha 12 colonne disponibili - Le colonne devono sommare a 12: es. [6,6] o [4,8] o [3,6,3] o [4,4,4] - Usa "col" per definire la larghezza: "col": 6 significa 6/12 = 50% width {$blocksReferenceText} {$examplesText} **FILOSOFIA DI DESIGN**: - Funzionalità SEMPRE prima, ma con personalità e stile - Coerenza con l'identità visiva, ma non rigidità creativa - Professionalità, ma con elementi distintivi - Gli esempi mostrano il "come tecnicamente fare", tu mostri il "cosa è creativamente possibile" - Ogni sezione deve avere un motivo per esistere ed essere memorabile **NOTA CRITICA SUGLI ESEMPI**: Gli esempi forniti sono RIFERIMENTI TECNICI per capire: - Come strutturare correttamente il JSON - Quali chiavi sono disponibili - Come annidare row > col > block NON sono template da copiare esteticamente. Usa la loro struttura tecnica ma crea il TUO design unico. **CHECKLIST PRE-RISPOSTA**: - [ ] La struttura JSON è corretta? (array di row con children col con children block) - [ ] Ho usato solo chiavi valide dal file di riferimento? - [ ] I colori vengono dalla visualIdentity? - [ ] I font vengono dalla visualIdentity? - [ ] Ho usato 'em' per line-height? - [ ] Le colonne sommano a 12? - [ ] Il design è funzionale E distintivo? - [ ] La lingua è corretta? - [ ] **I font size sono adeguati anche per mobile? (ridotti 30-40%)** - [ ] **Il contrasto colori è sufficiente? (testo chiaro su scuro, scuro su chiaro)** - [ ] **Ho specificato l'allineamento verticale (align-items) e orizzontale per ogni sezione?** - [ ] **Ogni sezione ha un background intenzionale (non tutto bianco)?** Rispondi SOLO con l'array JSON. Crea qualcosa di memorabile. PROMPT; } private function _buildSectionUserPrompt($sectionData, $sectionNumber, $totalSections, $globalContext = '', $existingRowJson = null) { // Pulizia del contesto per evitare caratteri strani $safeContext = strtoupper(trim($globalContext)); $customPrompt = ""; if($sectionData['type'] === 'header') { $customPrompt = "**IMPORTANTE**: In questo caso, copia la struttura dell'header in modo perfettamente identico dall'esempio allegato. Non inventare nessun parametro."; } // LOGICA IBRIDA: MODIFICA vs CREAZIONE if ($existingRowJson) { $taskContext = <<`, `

`) se appropriato, cambiando solo il testo interno. TXT; $generationTitle = 'MODIFICARE'; } else { $taskContext = << count($sectionRows), 'has_images' => false, 'has_buttons' => false, 'blocks' => [] ]; foreach ($sectionRows as $row) { if (!isset($row['children'])) continue; foreach ($row['children'] as $col) { if (!isset($col['children'])) continue; foreach ($col['children'] as $block) { $blockType = $block['block_type'] ?? 'unknown'; $summary['blocks'][] = $blockType; if (str_contains($blockType, 'image')) { $summary['has_images'] = true; } if (str_contains($blockType, 'button')) { $summary['has_buttons'] = true; } } } } $summary['blocks'] = array_unique($summary['blocks']); return $summary; } private function getSectionFriendlyName($type) { $names = [ 'header' => 'intestazione', 'hero' => 'sezione hero', 'features' => 'caratteristiche', 'testimonials' => 'testimonianze', 'pricing' => 'prezzi', 'about' => 'chi siamo', 'cta' => 'call-to-action', 'footer' => 'piè di pagina', 'contact' => 'contatti', 'gallery' => 'galleria', 'team' => 'team', 'faq' => 'domande frequenti' ]; return $names[$type] ?? $type; } } ----- File: importer.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\importer.php ---------------------------------------- stepsData = $stepsData; set_time_limit(900); $jobId = md5('import_v16_precision_' . trim(strtolower($url))); $wizard = \Container::get('\MSFramework\Framework\wizard'); $this->logMessage("=== IMPORT PAGE v16.2 - PRECISION MAPPING ==="); if ($cachedData = $this->checkCache($jobId)) { return $this->_postProcessJson($cachedData, 'import'); } $this->_loadReferenceFiles(); $wizard->setUpdates("{{Analizzo la pagina}}", "{{Scarico risorse...}}", self::WIZARD_ICON); $extractedData = $this->_extractPageData($url); if (isset($extractedData['error']) || empty($extractedData)) { return ['error' => "Scraping fallito."]; } $this->_buildEnhancedImageMap($extractedData); $cleanHtml = $this->_prepareCleanHtml($extractedData); $wizard->setUpdates("{{Scompongo il design}}", "{{Creo le slice...}}", self::WIZARD_ICON); $this->screenshotSlices = $this->_fetchSmartSlices($url, $jobId); if (empty($this->screenshotSlices)) return ['error' => "Slicing fallito."]; $wizard->setUpdates("{{Estraggo il DNA}}", "{{Definisco lo stile...}}", self::WIZARD_ICON); $this->pageDNA = $this->_analyzePageDNA($extractedData, [$this->screenshotSlices[0]]); $this->sectionContext['visualIdentity'] = $this->pageDNA['visualIdentity'] ?? []; $totalSlices = count($this->screenshotSlices); $wizard->setUpdateSteps(1, $totalSlices); $generatedRows = []; $lastRowContext = null; foreach ($this->screenshotSlices as $index => $slice) { $sliceNum = $index + 1; $detectedType = $this->_guessSectionTypeFromSlice([$slice]); $wizard->setUpdates("{{Analisi slice}} {$sliceNum}/{$totalSlices}", "{{Elaboro: " . strtoupper($detectedType) . "...}}", self::WIZARD_ICON); $bestExamples = $this->_selectBestExamples($detectedType, 'Replicazione visuale'); $examplesText = $this->_formatExamplesForPrompt($bestExamples); $sliceHtmlContext = !empty($slice['html_part']) ? $slice['html_part'] : $this->_getHtmlSnippet($cleanHtml, $index, $totalSlices); $aiResult = $this->_reconstructSectionFromSlice( $slice, $sliceHtmlContext, $this->pageDNA, $examplesText, $detectedType, $lastRowContext ); if (isset($aiResult['error'])) { $this->logMessage("ERRORE Slice {$index}: " . $aiResult['error']); continue; } $isMerge = $aiResult['merge_with_previous'] ?? false; if ($isMerge && isset($aiResult['updated_previous_row'])) { $this->logMessage(" -> MERGE DETECTED."); if (!empty($generatedRows)) array_pop($generatedRows); $mergedRow = $aiResult['updated_previous_row']; $normalized = $this->_autoCorrectSection([$mergedRow]); if(!empty($normalized)) { $generatedRows[] = $normalized[0]; $lastRowContext = $normalized[0]; } if (!empty($aiResult['new_additional_rows'])) { $addRows = $this->_autoCorrectSection($aiResult['new_additional_rows']); foreach($addRows as $ar) { $generatedRows[] = $ar; $lastRowContext = $ar; } } } else { $rawRows = isset($aiResult['rows']) ? $aiResult['rows'] : $aiResult; if (isset($rawRows['type'])) $rawRows = [$rawRows]; $rowsToProcess = $this->_autoCorrectSection($rawRows); foreach ($rowsToProcess as $row) { if ($index > 0 && $this->_isRepeatedHeaderRow($row)) continue; if ($index < ($totalSlices - 1) && $this->_isFooterRow($row)) continue; $generatedRows[] = $row; $lastRowContext = $row; } } $wizard->setUpdateSteps('+1'); } $wizard->setUpdates("{{Finalizzo}}", "{{Ottimizzo codice...}}", self::WIZARD_ICON); $this->_processImportedImagesEnhanced($generatedRows); $finalJson = $this->_assemblePageWithDNA($generatedRows, $this->pageDNA); $this->saveCache($jobId, $finalJson); $result = $this->_postProcessJson($finalJson, 'import'); $title = "{{Importazione}}: " . parse_url($url, PHP_URL_HOST); $this->saveToLibrary('import_page', $title, json_encode($result)); return $result; } // =================================================================== // METODI CORE: RECONSTRUCTION ENGINE // =================================================================== /** * Il cuore del nuovo importer. * Genera struttura e contenuto in un singolo passaggio, usando Vision + HTML Data. */ private function _reconstructSectionFromSlice($slice, $htmlContext, $pageDNA, $examplesText, $detectedType, $lastRowContext) { $prompts = $this->_buildReconstructionPrompt( $htmlContext, $pageDNA, $examplesText, $detectedType, $lastRowContext ); $config = ['temperature' => 0.2, 'topP' => 0.95, 'jsonMode' => true]; try { $response = $this->aiClient->generateContent($prompts['system'], $prompts['user'], $config, [$slice]); $jsonText = $this->_cleanJsonResponse($response->text()); $data = json_decode($jsonText, true); if (json_last_error() !== JSON_ERROR_NONE) { return ['error' => "JSON invalido: " . json_last_error_msg()]; } return $data; } catch (\Exception $e) { return ['error' => "Eccezione AI: " . $e->getMessage()]; } } private function _buildReconstructionPrompt($htmlContext, $pageDNA, $examplesText, $detectedType, $lastRowContext) { $dnaJson = json_encode($pageDNA['visualIdentity'] ?? [], JSON_PRETTY_PRINT); $blocksRef = $this->blocksReference ? json_encode($this->blocksReference, JSON_PRETTY_PRINT) : ""; $previousContextJson = $lastRowContext ? json_encode($lastRowContext, JSON_PRETTY_PRINT) : "null"; // --- UPDATE: CREAZIONE LEGENDA IMMAGINI --- // Questo aiuta l'AI a mappare visivamente "Fiore" -> "flower.jpg" -> {{IMG_1}} $imageLegend = "NESSUNA IMMAGINE RILEVATA IN QUESTA SEZIONE."; if (!empty($this->enhancedImageMap)) { $imageLegend = "USO OBBLIGATORIO DEI SEGUENTI TOKEN:\n"; $count = 0; foreach ($this->enhancedImageMap as $token => $info) { // Includiamo filename e dimensioni per aiutare il matching visivo $dims = isset($info['width']) ? "({$info['width']}x{$info['height']})" : ""; $imageLegend .= "- {$token} : {$info['filename']} {$dims}\n"; $count++; if ($count > 20) break; // Evitiamo prompt troppo lunghi } } $systemPrompt = << COL -> BLOCK. 2. Usa righe annidate per layout complessi. 3. Se vedi bottoni/link solo testo, usa `"background": { "color": "transparent" }`. DNA VISIVO: {$dnaJson} SCHEMA BLOCCHI: {$blocksRef} PROMPT; $userPrompt = << $systemPrompt, 'user' => $userPrompt]; } // =================================================================== // METODI DI SUPPORTO: ESTRAZIONE & ANALISI // =================================================================== private function _extractPageData($url) { $scraperUrl = $this->importerURL . '/scrape?url=' . urlencode($url); $content = $this->_downloadContent($scraperUrl); if (!$content) return false; $data = json_decode($content, true); return is_array($data) ? $data : false; } private function _fetchSmartSlices($url, $jobId) { $cacheFile = $this->cacheDir . '/slices_' . $jobId . '.json'; if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 86400)) { $cachedData = json_decode(file_get_contents($cacheFile), true); if (!empty($cachedData)) return $cachedData; } $apiUrl = $this->importerURL . '/screen-sliced?url=' . urlencode($url) . '&include_html=1'; $response = $this->_downloadContent($apiUrl); if (!$response) return []; $data = json_decode($response, true); if (!$data || empty($data['slices'])) return []; $processedSlices = []; foreach ($data['slices'] as $i => $sliceInfo) { if (empty($sliceInfo['url'])) continue; $imgContent = $this->_downloadContent($sliceInfo['url']); if (!$imgContent) continue; $base64 = base64_encode($imgContent); $processedSlices[] = [ 'index' => $i, 'mime' => 'image/png', 'data' => $base64, 'filename' => $sliceInfo['filename'] ?? "slice_{$i}.png", 'html_part' => $sliceInfo['html'] ?? '' ]; } $finalSlices = array_slice($processedSlices, 0, 10); if (!empty($finalSlices)) file_put_contents($cacheFile, json_encode($finalSlices)); return $finalSlices; } private function _analyzePageDNA($extractedData, $slices) { // Prepariamo i dati grezzi estratti dallo scraper $colors = json_encode($extractedData['info']['colors']['backgrounds'] ?? []); $fonts = json_encode($extractedData['info']['fonts'] ?? []); $systemPrompt = << 0.2, 'topP' => 0.95, 'jsonMode' => true]; // Passiamo le slice (di solito la prima) per analisi visiva $response = $this->aiClient->generateContent($systemPrompt, $userPrompt, $config, $slices); return json_decode($this->_cleanJsonResponse($response->text()), true); } catch (\Exception $e) { $this->logMessage("Errore analisi DNA: " . $e->getMessage()); // Fallback safe return ['visualIdentity' => ['isDarkMode' => false, 'colorPalette' => []]]; } } private function _buildEnhancedImageMap($extractedData) { $this->enhancedImageMap = []; $this->imageMap = []; $this->globalImageTokens = []; if (empty($extractedData['images'])) return; foreach ($extractedData['images'] as $i => $img) { $token = sprintf("{{IMG_%d}}", $i); $finalUrl = $img['download_url'] ?? $img['original_url'] ?? $img['url'] ?? ($img['attributes']['src'] ?? null); if (empty($finalUrl)) continue; $filename = $img['filename'] ?? basename(parse_url($finalUrl, PHP_URL_PATH)); // Pulizia nome file per aiutare l'AI $cleanFilename = preg_replace('/[%\+]/', '-', $filename); if (empty($cleanFilename) || strlen($cleanFilename) < 3) $cleanFilename = "image_{$i}.jpg"; $mapData = [ 'id' => $token, 'filename' => $cleanFilename, 'download_url' => $finalUrl, 'original_url' => $img['original_url'] ?? $finalUrl, 'directory' => 'PAGEGALLERY', 'width' => $img['width'] ?? 0, 'height' => $img['height'] ?? 0 ]; $this->enhancedImageMap[$token] = $mapData; $this->globalImageTokens[$token] = $mapData; } } private function _prepareCleanHtml($extractedData) { if (empty($extractedData['info']['clean_html'])) return ''; $html = $extractedData['info']['clean_html']; if (!empty($this->enhancedImageMap)) { $map = $this->enhancedImageMap; uasort($map, fn($a, $b) => strlen($b['download_url']) - strlen($a['download_url'])); foreach ($map as $token => $imgData) { if (!empty($imgData['download_url'])) { $html = str_replace($imgData['download_url'], $token, $html); } if (!empty($imgData['original_url'])) { $html = str_replace($imgData['original_url'], $token, $html); $parsed = parse_url($imgData['original_url']); if (isset($parsed['path']) && strlen($parsed['path']) > 4) { $html = str_replace($parsed['path'], $token, $html); } } if (!empty($imgData['filename']) && strlen($imgData['filename']) > 5) { $html = str_replace($imgData['filename'], $token, $html); } } } $html = preg_replace('/]*>(.*?)<\/script>/is', "", $html); $html = preg_replace('/]*>(.*?)<\/style>/is', "", $html); $html = preg_replace('/]*>(.*?)<\/svg>/is', "[SVG ICON]", $html); $html = preg_replace('//s', "", $html); $html = preg_replace('/\s+/', ' ', $html); return mb_substr($html, 0, 100000); } private function _getHtmlSnippet($fullHtml, $sliceIndex, $totalSlices) { $length = strlen($fullHtml); $chunkSize = ceil($length / $totalSlices); $start = max(0, ($sliceIndex * $chunkSize) - 500); $realChunk = $chunkSize + 1000; return mb_substr($fullHtml, $start, $realChunk); } // =================================================================== // METODI DI ASSEMBLAGGIO E FIXING // =================================================================== /** * FIX IMMAGINI v3.5 (Structural Reshaping) * Converte chiavi errate (path, src) in 'files' e forza il formato [['PAGEGALLERY', 'file']]. */ private function _processImportedImagesEnhanced(&$rows) { $this->logMessage("=== FIX IMMAGINI UNIVERSALE (Reshaping) ==="); $tokenRegex = '/(?:{{\s*)?IMG_(\d+)(?:\s*}})?/i'; $resolver = function($input) use ($tokenRegex) { // 1. TOKEN if (is_string($input) && preg_match($tokenRegex, $input, $matches)) { return $this->_resolveToken($matches[1]); } // 2. URL if (is_string($input) && (str_starts_with($input, 'http') || str_starts_with($input, '//'))) { if (str_starts_with($input, '//')) $input = 'https:' . $input; return $this->_saveImageFromUrl($input); } return null; }; // Walker ricorsivo che può modificare la struttura dell'array padre $fixer = function(&$node) use (&$fixer, $resolver) { if (!is_array($node)) return; // 1. PRE-CHECK: Decodifica JSON stringified su 'files' se presente if (isset($node['files']) && is_string($node['files']) && str_starts_with(trim($node['files']), '[')) { $decoded = json_decode($node['files'], true); if (is_array($decoded)) $node['files'] = $decoded; } // 2. ITERAZIONE CHIAVI // Usiamo array_keys per poter modificare $node in-place senza rompere il loop $keys = array_keys($node); foreach ($keys as $key) { // Riferimento al valore attuale $val = $node[$key]; // --- RICORSIONE PRIMA DI TUTTO --- if (is_array($val)) { $fixer($node[$key]); continue; } // --- RISOLUZIONE VALORE --- if (is_string($val)) { $resolved = $resolver($val); if ($resolved) { // Abbiamo un file locale valido (scaricato o risolto) // Formato standard Visual Builder: $builderFormat = [['PAGEGALLERY', $resolved['filename']]]; // CASO A: La chiave è già 'files' -> Aggiorna solo il valore if ($key === 'files') { $node['files'] = $builderFormat; } // CASO B: La chiave è 'path', 'src', 'url' -> RISTRUTTURA // Elimina la chiave vecchia (es. path) e crea 'files' elseif (in_array($key, ['path', 'src', 'url'])) { $node['files'] = $builderFormat; unset($node[$key]); } // CASO C: La chiave è 'image' o 'background' ed è una stringa diretta // Es: "image": "{{IMG_1}}" -> diventa "image": { "files": [...] } elseif ($key === 'image') { $node['image'] = ['files' => $builderFormat]; } elseif ($key === 'background') { // Se background era solo una stringa URL, lo espandiamo in oggetto $node['background'] = [ 'image' => ['files' => $builderFormat, 'size' => 'cover', 'position' => 'center'], 'color' => '' // Reset colore per dare priorità img ]; } } } } }; foreach ($rows as &$row) { $fixer($row); } } // Helper privato per risolvere e scaricare (DRY) private function _resolveToken($id) { $tokenString = "{{IMG_" . $id . "}}"; // Ricostruiamo formato standard mappa $saved = $this->_resolveAndDownloadImage($tokenString); if ($saved) { $this->logMessage(" -> Risolto IMG_{$id} (trovato in array/stringa) -> {$saved['filename']}"); return $saved; } return null; } private function _resolveAndDownloadImage($token) { $cleanTokenID = preg_replace('/[^{}\w]/', '', $token); // Rimuove caratteri strani $cleanTokenID = str_replace(['{{', '}}'], '', $cleanTokenID); // Rimuove graffe // Cerchiamo nella mappa // La mappa usa chiavi con graffe "{{IMG_0}}", quindi dobbiamo ricostruire quella chiave per il lookup diretto $mapKey = "{{" . $cleanTokenID . "}}"; if (isset($this->enhancedImageMap[$mapKey])) { $info = $this->enhancedImageMap[$mapKey]; if(!$info['download_url']) { \Container::get('ErrorTracker')->console($info, 'MissingData'); return null; } // Download effettivo $saved = $this->_saveImageFromUrl($info['download_url']); \Container::get('ErrorTracker')->console($saved, 'SavedData'); if ($saved) { return $saved; } } return null; } private function _assemblePageWithDNA($generatedRows, $pageDNA) { $globalCss = ""; $visual = $pageDNA['visualIdentity'] ?? []; // Global Background if (isset($visual['globalBackground']['cssValue'])) { $bg = $visual['globalBackground']['cssValue']; if ($bg && $bg !== '#ffffff') { $globalCss .= "body, .visual-builder-container { background: {$bg} !important; } "; } } // Global Fonts if (isset($visual['typography']['headingFont'])) { $hFont = $visual['typography']['headingFont']; $globalCss .= "h1, h2, h3, h4, h5, h6, .display-1, .display-2 { font-family: '{$hFont}', sans-serif !important; } "; } if (isset($visual['typography']['bodyFont'])) { $bFont = $visual['typography']['bodyFont']; $globalCss .= "body, p, li, span, div { font-family: '{$bFont}', sans-serif !important; } "; } $settingsRow = [ 'type' => 'settings', 'json' => [ 'css' => $globalCss, 'js' => '', 'deviceOnly' => [] ] ]; $allRows = array_merge($generatedRows, [$settingsRow]); return ['rows' => $allRows]; } // =================================================================== // HELPER UTILITY // =================================================================== private function _guessSectionTypeFromSlice($slices) { $system = "Sei un analista UI. Rispondi con UNA parola: header, hero, features, testimonials, pricing, gallery, contact, footer, content."; $user = "Classifica questa sezione."; try { $config = ['temperature' => 0.1, 'maxOutputTokens' => 50]; $res = $this->aiClient->generateContent($system, $user, $config, $slices); $text = strtolower(trim($this->_cleanJsonResponse($res->text()))); return preg_replace('/[^a-z]/', '', $text); } catch (\Exception $e) { return 'generic'; } } private function _isRepeatedHeaderRow($row) { if (($row['type'] ?? '') !== 'row') return false; $hasLogo = false; $hasMenu = false; // Scansione profonda array_walk_recursive($row, function($val, $key) use (&$hasLogo, &$hasMenu) { if ($key === 'block_type') { if ($val === 'main/logo') $hasLogo = true; if ($val === 'main/menu' || $val === 'main/sitemap') $hasMenu = true; } }); return ($hasLogo && $hasMenu); } private function _isFooterRow($row) { // Euristica semplice: se contiene copyright o link privacy e non è l'ultima slice $jsonStr = json_encode($row); return (stripos($jsonStr, 'copyright') !== false || stripos($jsonStr, 'privacy policy') !== false); } } ----- File: client.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\Engines\client.php ---------------------------------------- 'image/png', 'data' => 'base64string...'] * @return object La risposta dall'API, che deve includere almeno un metodo text() e usageMetadata. */ public function generateContent(string $systemPrompt, string $userPrompt, array $config, array $images = []): object; } ----- File: factory.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\Engines\factory.php ---------------------------------------- client = \Gemini::factory() ->withApiKey($apiKey) ->withHttpClient(new \GuzzleHttp\Client(['timeout' => 60 * 6])) ->make(); } public function generateContent(string $systemPrompt, string $userPrompt, array $config, array $images = []): object { // --- SISTEMA DI CACHE --- $enableCache = true; //!empty($config['enable_cache']); // "Pulsante" di abilitazione $cacheFile = null; if ($enableCache) { $cacheDir = __DIR__ . '/../cache/'; // Assicuriamoci che la cartella esista if (!is_dir($cacheDir)) { @mkdir($cacheDir, 0777, true); } // Creiamo un ID univoco basato su tutti i parametri di input // (Serializziamo anche le immagini per assicurarci che se cambia l'immagine, cambia la cache) $cacheKey = md5(serialize([$systemPrompt, $userPrompt, $config, $images])); $cacheFile = $cacheDir . $cacheKey . '.json'; // SE ESISTE LA CACHE: Restituisci subito la risposta salvata if (file_exists($cacheFile)) { $cachedData = json_decode(file_get_contents($cacheFile), true); if ($cachedData) { // Ritorniamo un oggetto anonimo strutturato come quello originale return new class($cachedData) { private $text; public $usageMetadata; public function __construct($data) { $this->text = $data['text'] ?? ''; // Ricostruiamo l'oggetto metadata (o array, a seconda di come viene usato fuori) $this->usageMetadata = isset($data['usageMetadata']) ? (object)$data['usageMetadata'] : null; } public function text(): string { return $this->text; } }; } } } // ------------------------ $parts = []; // 1. Aggiungi il prompt di sistema e utente combinati $fullPromptText = $systemPrompt . "\n\n" . $userPrompt; $parts[] = new Part(text: $fullPromptText); // 2. Aggiungi le immagini se presenti if (!empty($images)) { foreach ($images as $img) { if (!empty($img['data'])) { $cleanBase64 = preg_replace('#^data:image/\w+;base64,#i', '', $img['data']); $parts[] = new Part(inlineData: new Blob( mimeType: ($img['mime'] ? \Gemini\Enums\MimeType::from($img['mime']) : \Gemini\Enums\MimeType::IMAGE_PNG), data: $cleanBase64 )); } } } $useUrlContext = preg_match('/https?:\/\//i', $fullPromptText) && empty($images); $generationConfig = new GenerationConfig( maxOutputTokens: $config['maxOutputTokens'] ?? 65536, temperature: $config['temperature'] ?? 0.7, topP: $config['topP'] ?? 0.9, topK: $config['topK'] ?? 40, responseMimeType: $config['jsonMode'] && !$useUrlContext ? ResponseMimeType::APPLICATION_JSON : ResponseMimeType::TEXT_PLAIN ); $model = $this->client ->generativeModel(model: self::GEMINI_DEFAULT_MODEL) ->withGenerationConfig($generationConfig); if ($useUrlContext) { $model = $model->withTool(new Tool(urlContext: new UrlContext())); } if (!empty($images)) { $contentObj = new Content(parts: $parts, role: Role::USER); $response = $model->generateContent($contentObj); } else { $response = $model->generateContent($fullPromptText); } // Creiamo l'oggetto risultato (LIVE) $resultObject = new class($response) { private $data; public $usageMetadata; public function __construct($data) { $this->data = $data; $meta = $data->usageMetadata; $this->usageMetadata = (object) [ 'totalTokenCount' => $meta->totalTokenCount ?? 0, 'promptTokenCount' => $meta->promptTokenCount ?? 0, 'candidatesTokenCount' => $meta->candidatesTokenCount ?? 0 ]; } public function text(): string { $text = ''; if (!empty($this->data->candidates[0]->content->parts)) { foreach ($this->data->candidates[0]->content->parts as $part) { $text .= $part->text ?? ''; } } return $text; } }; // --- SALVATAGGIO CACHE --- if ($enableCache && $cacheFile) { // Estraiamo il testo e i metadati per salvarli $contentToCache = [ 'text' => $resultObject->text(), 'usageMetadata' => $resultObject->usageMetadata, 'prompt' => $fullPromptText, 'files' => $images ]; file_put_contents($cacheFile, json_encode($contentToCache)); } // ------------------------- return $resultObject; } } ----- File: grok.php Path completo: C:\Users\HP\Documents\Progetti\Framework\classes\core\AI\generator\Engines\grok.php ---------------------------------------- apiKey = $apiKey; $this->client = new GuzzleClient([ 'base_uri' => self::API_ENDPOINT, 'timeout' => 60 * 3, ]); } public function generateContent(string $systemPrompt, string $userPrompt, array $config): object { $requestBody = [ 'model' => self::GROK_DEFAULT_MODEL, 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt] ], 'temperature' => $config['temperature'] ?? 0.7, 'max_tokens' => $config['maxOutputTokens'] ?? 8192, 'top_p' => $config['topP'] ?? 0.9, ]; if ($config['jsonMode']) { $requestBody['response_format'] = ['type' => 'json_object']; } try { $response = $this->client->post('', [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->apiKey, 'Content-Type' => 'application/json', ], 'json' => $requestBody ]); $body = json_decode($response->getBody()->getContents(), true); // Creiamo un oggetto standard per la risposta, compatibile con quello di Gemini return new class($body) { private $data; public $usageMetadata; public function __construct($data) { $this->data = $data; $this->usageMetadata = (object) [ 'totalTokenCount' => $data['usage']['total_tokens'] ?? 0 ]; } public function text(): string { return $this->data['choices'][0]['message']['content'] ?? ''; } }; } catch (RequestException $e) { $errorMessage = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : $e->getMessage(); throw new \Exception("Grok API Error: " . $errorMessage); } } }