From d39bcc532bec4f622e0dd0067191f4a41c669aed Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 9 Nov 2025 21:15:36 +0000 Subject: [PATCH] 1 --- .perm_test_apache | 0 .perm_test_exec | 0 ai/LocalAIApi.php | 311 ++++++++++++++++++++++++++++++++++++++++++ ai/config.php | 52 +++++++ assets/css/custom.css | 95 +++++++++++++ assets/js/main.js | 147 ++++++++++++++++++++ index.php | 242 +++++++++++++------------------- 7 files changed, 702 insertions(+), 145 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 ai/LocalAIApi.php create mode 100644 ai/config.php create mode 100644 assets/css/custom.css create mode 100644 assets/js/main.js diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php new file mode 100644 index 0000000..00b1b00 --- /dev/null +++ b/ai/LocalAIApi.php @@ -0,0 +1,311 @@ + [ +// ['role' => 'system', 'content' => 'You are a helpful assistant.'], +// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], +// ], +// ]); +// if (!empty($response['success'])) { +// $decoded = LocalAIApi::decodeJsonFromResponse($response); +// } + +class LocalAIApi +{ + /** @var array|null */ + private static ?array $configCache = null; + + /** + * Signature compatible with the OpenAI Responses API. + * + * @param array $params Request body (model, input, text, reasoning, metadata, etc.). + * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). + * @return array{ + * success:bool, + * status?:int, + * data?:mixed, + * error?:string, + * response?:mixed, + * message?:string + * } + */ + public static function createResponse(array $params, array $options = []): array + { + $cfg = self::config(); + $payload = $params; + + if (empty($payload['input']) || !is_array($payload['input'])) { + return [ + 'success' => false, + 'error' => 'input_missing', + 'message' => 'Parameter "input" is required and must be an array.', + ]; + } + + if (!isset($payload['model']) || $payload['model'] === '') { + $payload['model'] = $cfg['default_model']; + } + + return self::request($options['path'] ?? null, $payload, $options); + } + + /** + * Snake_case alias for createResponse (matches the provided example). + * + * @param array $params + * @param array $options + * @return array + */ + public static function create_response(array $params, array $options = []): array + { + return self::createResponse($params, $options); + } + + /** + * Perform a raw request to the AI proxy. + * + * @param string $path Endpoint (may be an absolute URL). + * @param array $payload JSON payload. + * @param array $options Additional request options. + * @return array + */ + public static function request(?string $path = null, array $payload = [], array $options = []): array + { + if (!function_exists('curl_init')) { + return [ + 'success' => false, + 'error' => 'curl_missing', + 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', + ]; + } + + $cfg = self::config(); + + $projectUuid = $cfg['project_uuid']; + if (empty($projectUuid)) { + return [ + 'success' => false, + 'error' => 'project_uuid_missing', + 'message' => 'PROJECT_UUID is not defined; aborting AI request.', + ]; + } + + $defaultPath = $cfg['responses_path'] ?? null; + $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); + if (empty($resolvedPath)) { + return [ + 'success' => false, + 'error' => 'project_id_missing', + 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', + ]; + } + + $url = self::buildUrl($resolvedPath, $cfg['base_url']); + $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; + $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; + if ($timeout <= 0) { + $timeout = 30; + } + + $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; + $verifyTls = array_key_exists('verify_tls', $options) + ? (bool) $options['verify_tls'] + : $baseVerifyTls; + + $projectHeader = $cfg['project_header']; + + $headers = [ + 'Content-Type: application/json', + 'Accept: application/json', + ]; + $headers[] = $projectHeader . ': ' . $projectUuid; + if (!empty($options['headers']) && is_array($options['headers'])) { + foreach ($options['headers'] as $header) { + if (is_string($header) && $header !== '') { + $headers[] = $header; + } + } + } + + if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { + $payload['project_uuid'] = $projectUuid; + } + + $body = json_encode($payload, JSON_UNESCAPED_UNICODE); + if ($body === false) { + return [ + 'success' => false, + 'error' => 'json_encode_failed', + 'message' => 'Failed to encode request body to JSON.', + ]; + } + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + + $responseBody = curl_exec($ch); + if ($responseBody === false) { + $error = curl_error($ch) ?: 'Unknown cURL error'; + curl_close($ch); + return [ + 'success' => false, + 'error' => 'curl_error', + 'message' => $error, + ]; + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $decoded = null; + if ($responseBody !== '' && $responseBody !== null) { + $decoded = json_decode($responseBody, true); + if (json_last_error() !== JSON_ERROR_NONE) { + $decoded = null; + } + } + + if ($status >= 200 && $status < 300) { + return [ + 'success' => true, + 'status' => $status, + 'data' => $decoded ?? $responseBody, + ]; + } + + $errorMessage = 'AI proxy request failed'; + if (is_array($decoded)) { + $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; + } elseif (is_string($responseBody) && $responseBody !== '') { + $errorMessage = $responseBody; + } + + return [ + 'success' => false, + 'status' => $status, + 'error' => $errorMessage, + 'response' => $decoded ?? $responseBody, + ]; + } + + /** + * Extract plain text from a Responses API payload. + * + * @param array $response Result of LocalAIApi::createResponse|request. + * @return string + */ + public static function extractText(array $response): string + { + $payload = $response['data'] ?? $response; + if (!is_array($payload)) { + return ''; + } + + if (!empty($payload['output']) && is_array($payload['output'])) { + $combined = ''; + foreach ($payload['output'] as $item) { + if (!isset($item['content']) || !is_array($item['content'])) { + continue; + } + foreach ($item['content'] as $block) { + if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { + $combined .= $block['text']; + } + } + } + if ($combined !== '') { + return $combined; + } + } + + if (!empty($payload['choices'][0]['message']['content'])) { + return (string) $payload['choices'][0]['message']['content']; + } + + return ''; + } + + /** + * Attempt to decode JSON emitted by the model (handles markdown fences). + * + * @param array $response + * @return array|null + */ + public static function decodeJsonFromResponse(array $response): ?array + { + $text = self::extractText($response); + if ($text === '') { + return null; + } + + $decoded = json_decode($text, true); + if (is_array($decoded)) { + return $decoded; + } + + $stripped = preg_replace('/^```json|```$/m', '', trim($text)); + if ($stripped !== null && $stripped !== $text) { + $decoded = json_decode($stripped, true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } + + /** + * Load configuration from ai/config.php. + * + * @return array + */ + private static function config(): array + { + if (self::$configCache === null) { + $configPath = __DIR__ . '/config.php'; + if (!file_exists($configPath)) { + throw new RuntimeException('AI config file not found: ai/config.php'); + } + $cfg = require $configPath; + if (!is_array($cfg)) { + throw new RuntimeException('Invalid AI config format: expected array'); + } + self::$configCache = $cfg; + } + + return self::$configCache; + } + + /** + * Build an absolute URL from base_url and a path. + */ + private static function buildUrl(string $path, string $baseUrl): string + { + $trimmed = trim($path); + if ($trimmed === '') { + return $baseUrl; + } + if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { + return $trimmed; + } + if ($trimmed[0] === '/') { + return $baseUrl . $trimmed; + } + return $baseUrl . '/' . $trimmed; + } +} + +// Legacy alias for backward compatibility with the previous class name. +if (!class_exists('OpenAIService')) { + class_alias(LocalAIApi::class, 'OpenAIService'); +} diff --git a/ai/config.php b/ai/config.php new file mode 100644 index 0000000..1ba1596 --- /dev/null +++ b/ai/config.php @@ -0,0 +1,52 @@ + $baseUrl, + 'responses_path' => $responsesPath, + 'project_id' => $projectId, + 'project_uuid' => $projectUuid, + 'project_header' => 'project-uuid', + 'default_model' => 'gpt-5', + 'timeout' => 30, + 'verify_tls' => true, +]; diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..4e20b74 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,95 @@ + +/* assets/css/custom.css */ + +@import url('https://fonts.googleapis.com/css2?family=Mountains+of+Christmas:wght@700&family=Lato:wght@400;700&display=swap'); + +body { + font-family: 'Lato', sans-serif; + background-color: #F0F8FF; + color: #292B2C; + overflow-x: hidden; +} + +h1, h2, h3, .h1, .h2, .h3 { + font-family: 'Mountains of Christmas', cursive; + font-weight: 700; +} + +.navbar-brand { + font-family: 'Mountains of Christmas', cursive; +} + +.card { + border-radius: 0.5rem; + border: none; + box-shadow: 0 4px 15px rgba(0,0,0,0.05); +} + +.btn-primary { + background-color: #D9534F; + border-color: #D9534F; + transition: background-color 0.3s ease; +} + +.btn-primary:hover { + background-color: #c9302c; + border-color: #c9302c; +} + +.btn-secondary { + background-color: #5CB85C; + border-color: #5CB85C; + transition: background-color 0.3s ease; +} + +.btn-secondary:hover { + background-color: #4cae4c; + border-color: #4cae4c; +} + +.btn-danger { + background-color: #F0AD4E; + border-color: #F0AD4E; + transition: background-color 0.3s ease; +} + +.btn-danger:hover { + background-color: #ec971f; + border-color: #ec971f; +} + +#shopping-list-container { + background-color: #fff; + padding: 2rem; + border-radius: 0.5rem; + min-height: 300px; +} + +.ingredient-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.ingredient-row .form-control { + flex: 1; +} + +/* Snowflakes animation */ +.snowflake { + color: #fff; + font-size: 1em; + font-family: Arial, sans-serif; + text-shadow: 0 0 5px #000; + position: fixed; + top: -5%; + z-index: -1; + user-select: none; + animation: fall linear infinite; +} + +@keyframes fall { + to { + transform: translateY(105vh); + } +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..bcb58f1 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,147 @@ + +document.addEventListener('DOMContentLoaded', function () { + + // --- Snowflakes Effect --- + function createSnowflakes() { + const snowflakeContainer = document.body; + for (let i = 0; i < 50; i++) { + const snowflake = document.createElement('div'); + snowflake.className = 'snowflake'; + snowflake.textContent = '❄'; + snowflake.style.left = Math.random() * 100 + 'vw'; + snowflake.style.animationDuration = (Math.random() * 3 + 2) + 's'; // 2-5 seconds + snowflake.style.animationDelay = Math.random() * 2 + 's'; + snowflake.style.opacity = Math.random(); + snowflake.style.fontSize = Math.random() * 10 + 10 + 'px'; + snowflakeContainer.appendChild(snowflake); + } + } + + createSnowflakes(); + + // --- Calculator Logic --- + const ingredientsContainer = document.getElementById('ingredients-container'); + const addIngredientBtn = document.getElementById('add-ingredient'); + const calculateBtn = document.getElementById('calculate-btn'); + const shoppingListContainer = document.getElementById('shopping-list-container'); + + let ingredientIndex = 1; + + function addIngredientRow() { + ingredientIndex++; + const row = document.createElement('div'); + row.className = 'ingredient-row mb-2'; + row.innerHTML = ` + + + + + `; + ingredientsContainer.appendChild(row); + } + + if (addIngredientBtn) { + addIngredientBtn.addEventListener('click', addIngredientRow); + } + + if (ingredientsContainer) { + ingredientsContainer.addEventListener('click', function(e) { + if (e.target.classList.contains('remove-ingredient')) { + e.target.closest('.ingredient-row').remove(); + } + }); + } + + if (calculateBtn) { + calculateBtn.addEventListener('click', function() { + const recipeName = document.getElementById('recipeName').value || 'My Festive Recipe'; + const guestCount = parseInt(document.getElementById('guestCount').value, 10); + + if (isNaN(guestCount) || guestCount <= 0) { + alert('Please enter a valid number of guests.'); + return; + } + + const ingredients = []; + const rows = ingredientsContainer.querySelectorAll('.ingredient-row'); + rows.forEach(row => { + const name = row.children[0].value; + const qty = parseFloat(row.children[1].value); + const unit = row.children[2].value; + + if (name && !isNaN(qty) && qty > 0) { + ingredients.push({ name, qty, unit }); + } + }); + + if (ingredients.length === 0) { + alert('Please add at least one ingredient.'); + return; + } + + // Calculate totals + const shoppingList = {}; + ingredients.forEach(ing => { + const totalQty = ing.qty * guestCount; + const key = ing.name.toLowerCase().trim() + '_' + (ing.unit || '').toLowerCase().trim(); + + if (shoppingList[key]) { + shoppingList[key].qty += totalQty; + } else { + shoppingList[key] = { + name: ing.name, + qty: totalQty, + unit: ing.unit + }; + } + }); + + // Render shopping list + renderShoppingList(recipeName, guestCount, Object.values(shoppingList)); + }); + } + + function renderShoppingList(recipeName, guestCount, list) { + let html = `

${recipeName} - Shopping List for ${guestCount} Guests


`; + + if (list.length === 0) { + html += '

No ingredients to show. Please fill out the recipe form.

'; + } else { + html += '
    '; + list.forEach(item => { + html += `
  • + ${item.name} + ${formatQuantity(item.qty)} ${item.unit} +
  • `; + }); + html += '
'; + } + + shoppingListContainer.innerHTML = html; + } + + function formatQuantity(qty) { + // Simple formatting, can be expanded for fractions + return parseFloat(qty.toFixed(2)); + } + + const newRecipeBtn = document.getElementById('new-recipe-btn'); + + if (newRecipeBtn) { + newRecipeBtn.addEventListener('click', function() { + document.getElementById('recipeName').value = ''; + document.getElementById('guestCount').value = ''; + ingredientsContainer.innerHTML = ''; + addIngredientRow(); // Add a fresh row + shoppingListContainer.innerHTML = ` +
+

Your Shopping List

+

Your calculated list will appear here.

+
+ `; + }); + } + + // Add one ingredient row by default + addIngredientRow(); +}); diff --git a/index.php b/index.php index 7205f3d..fd979d1 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,102 @@ - - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + + + Christmas Recipe Calculator + + + + + + + + + + + + + + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
-
- Page updated: (UTC) -
+ +
+ + + +
+
+

Holiday Recipe Planner

+

Enter a recipe for one person, set your guest count, and we'll make your shopping list!

+
+ +
+ +
+
+

Your Recipe

+
+
+ + +
+ +
+ +

Ingredients (for 1 person)

+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+
+
+
+

Your Shopping List

+

Your calculated list will appear here.

+
+
+
+
+
+
+ +
+

© Christmas Recipe Calculator. Happy Holidays!

+
+ + + + + - + \ No newline at end of file