Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,55 +1,362 @@
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import uuid
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
import google.generativeai as genai
|
|
|
|
|
|
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
-
import logging
|
| 10 |
-
import socket # For finding available port
|
| 11 |
|
| 12 |
-
|
|
|
|
| 13 |
|
| 14 |
app = Flask(__name__)
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
# ---
|
| 17 |
-
|
|
|
|
|
|
|
| 18 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 19 |
-
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
#
|
| 30 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
if
|
| 38 |
-
|
| 39 |
-
if
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
<!DOCTYPE html>
|
| 48 |
<html lang="ru">
|
| 49 |
<head>
|
| 50 |
<meta charset="UTF-8">
|
| 51 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 52 |
-
<title>EVA - Генератор
|
|
|
|
| 53 |
<style>
|
| 54 |
:root {
|
| 55 |
--system-gray-100-light: #f2f2f7;
|
|
@@ -105,7 +412,7 @@ html_template = """
|
|
| 105 |
--secondary-text-color: var(--system-gray-light-75-light);
|
| 106 |
--tertiary-text-color: var(--system-gray-light-50-light);
|
| 107 |
--border-color: var(--system-separator-light);
|
| 108 |
-
|
| 109 |
--input-bg: var(--system-gray-75-light);
|
| 110 |
--primary-color: var(--system-blue-light);
|
| 111 |
--primary-color-hover: var(--system-blue-light-hover);
|
|
@@ -114,7 +421,7 @@ html_template = """
|
|
| 114 |
}
|
| 115 |
|
| 116 |
html {
|
| 117 |
-
|
| 118 |
}
|
| 119 |
|
| 120 |
body {
|
|
@@ -138,7 +445,7 @@ html_template = """
|
|
| 138 |
padding: 25px 30px 30px 30px;
|
| 139 |
border-radius: 24px;
|
| 140 |
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
|
| 141 |
-
max-width:
|
| 142 |
width: calc(100% - 40px);
|
| 143 |
box-sizing: border-box;
|
| 144 |
margin-top: 30px;
|
|
@@ -190,9 +497,9 @@ html_template = """
|
|
| 190 |
}
|
| 191 |
|
| 192 |
textarea#prompt-input:focus {
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
}
|
| 197 |
|
| 198 |
button#generate-button {
|
|
@@ -227,10 +534,10 @@ html_template = """
|
|
| 227 |
}
|
| 228 |
|
| 229 |
.output-header {
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
}
|
| 235 |
|
| 236 |
label#output-label {
|
|
@@ -250,24 +557,24 @@ html_template = """
|
|
| 250 |
padding: 5px 8px;
|
| 251 |
border-radius: 6px;
|
| 252 |
transition: background-color 0.2s ease, color 0.2s ease;
|
| 253 |
-
display: none;
|
| 254 |
}
|
| 255 |
|
| 256 |
button#copy-button:hover {
|
| 257 |
background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
|
| 258 |
}
|
| 259 |
-
|
| 260 |
button#copy-button:active {
|
| 261 |
background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
|
| 262 |
}
|
| 263 |
-
|
| 264 |
button#copy-button.copied {
|
| 265 |
-
|
| 266 |
}
|
| 267 |
@media (prefers-color-scheme: dark) {
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
}
|
| 272 |
|
| 273 |
#output-container {
|
|
@@ -285,7 +592,7 @@ html_template = """
|
|
| 285 |
align-items: center;
|
| 286 |
justify-content: center;
|
| 287 |
}
|
| 288 |
-
|
| 289 |
#output-container a {
|
| 290 |
color: var(--primary-color);
|
| 291 |
text-decoration: none;
|
|
@@ -296,9 +603,8 @@ html_template = """
|
|
| 296 |
text-decoration: underline;
|
| 297 |
}
|
| 298 |
|
| 299 |
-
|
| 300 |
#output-container.loading::before {
|
| 301 |
-
content: "Генерация
|
| 302 |
display: block;
|
| 303 |
text-align: center;
|
| 304 |
font-style: italic;
|
|
@@ -319,7 +625,142 @@ html_template = """
|
|
| 319 |
50% { opacity: 1; }
|
| 320 |
}
|
| 321 |
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
body {
|
| 324 |
padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
|
| 325 |
align-items: flex-start;
|
|
@@ -333,55 +774,105 @@ html_template = """
|
|
| 333 |
h1 {
|
| 334 |
font-size: 28px;
|
| 335 |
}
|
| 336 |
-
|
| 337 |
font-size: 16px;
|
| 338 |
margin-bottom: 25px;
|
| 339 |
}
|
| 340 |
.form-group {
|
| 341 |
margin-bottom: 22px;
|
| 342 |
}
|
| 343 |
-
|
| 344 |
padding: 12px 15px;
|
| 345 |
min-height: 100px;
|
| 346 |
-
|
| 347 |
button#generate-button {
|
| 348 |
-
|
| 349 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
}
|
| 351 |
-
#output-container {
|
| 352 |
-
padding: 15px 18px;
|
| 353 |
-
font-size: 14px;
|
| 354 |
-
min-height: 50px;
|
| 355 |
-
}
|
| 356 |
-
.output-section {
|
| 357 |
-
margin-top: 30px;
|
| 358 |
-
}
|
| 359 |
}
|
| 360 |
</style>
|
| 361 |
</head>
|
| 362 |
<body>
|
| 363 |
<div class="container">
|
| 364 |
<h1>EVA</h1>
|
| 365 |
-
<p class="subtitle">Генератор
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
<form id="generate-form">
|
| 368 |
<div class="form-group">
|
| 369 |
-
<label for="prompt-input" class="input-label">Опишите
|
| 370 |
-
<textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай
|
| 371 |
</div>
|
| 372 |
|
| 373 |
-
<button type="submit" id="generate-button">Создать
|
| 374 |
</form>
|
| 375 |
|
| 376 |
<div class="output-section">
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
<div id="output-container" aria-live="polite">
|
| 382 |
-
<!-- Сообщение о том, что нужно ввести для начала -->
|
| 383 |
</div>
|
| 384 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
</div>
|
| 386 |
|
| 387 |
<script>
|
|
@@ -395,7 +886,7 @@ html_template = """
|
|
| 395 |
event.preventDefault();
|
| 396 |
|
| 397 |
if (!promptInput.value.trim()) {
|
| 398 |
-
showError("Пожалуйста, опишите
|
| 399 |
return;
|
| 400 |
}
|
| 401 |
|
|
@@ -403,7 +894,7 @@ html_template = """
|
|
| 403 |
|
| 404 |
generateButton.disabled = true;
|
| 405 |
generateButton.textContent = 'Генерация...';
|
| 406 |
-
outputContainer.innerHTML = '';
|
| 407 |
outputContainer.classList.add('loading');
|
| 408 |
outputContainer.classList.remove('error');
|
| 409 |
copyButton.style.display = 'none';
|
|
@@ -425,33 +916,29 @@ html_template = """
|
|
| 425 |
if (result.site_url) {
|
| 426 |
const link = document.createElement('a');
|
| 427 |
link.href = result.site_url;
|
| 428 |
-
link.textContent = "Открыть
|
| 429 |
-
link.target = "_blank";
|
| 430 |
-
outputContainer.innerHTML = '';
|
| 431 |
outputContainer.appendChild(link);
|
| 432 |
copyButton.style.display = 'block';
|
| 433 |
-
copyButton.dataset.copyText =
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
appIdText.style.marginTop = "10px";
|
| 439 |
-
appIdText.style.textAlign = "center";
|
| 440 |
-
outputContainer.appendChild(appIdText);
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
} else if (result.error) {
|
| 444 |
showError(result.error);
|
| 445 |
} else {
|
| 446 |
-
showError("Не удалось получить ссылку на
|
| 447 |
}
|
| 448 |
|
| 449 |
} catch (error) {
|
| 450 |
console.error("Fetch Error:", error);
|
| 451 |
showError(`Ошибка: ${error.message}`);
|
|
|
|
| 452 |
} finally {
|
| 453 |
generateButton.disabled = false;
|
| 454 |
-
generateButton.textContent = 'Создать
|
| 455 |
outputContainer.classList.remove('loading');
|
| 456 |
}
|
| 457 |
});
|
|
@@ -464,291 +951,246 @@ html_template = """
|
|
| 464 |
copyButton.textContent = 'Скопировано!';
|
| 465 |
copyButton.classList.add('copied');
|
| 466 |
setTimeout(() => {
|
| 467 |
-
|
| 468 |
-
|
| 469 |
}, 1500);
|
| 470 |
}).catch(err => {
|
| 471 |
console.error('Ошибка копирования: ', err);
|
| 472 |
copyButton.textContent = 'Ошибка';
|
| 473 |
-
|
| 474 |
-
|
| 475 |
}, 1500);
|
| 476 |
});
|
| 477 |
});
|
| 478 |
|
| 479 |
function showError(message) {
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
}
|
|
|
|
| 488 |
</script>
|
| 489 |
</body>
|
| 490 |
</html>
|
| 491 |
"""
|
| 492 |
|
| 493 |
-
#
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
#
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
1. **Single File:** All Python code, HTML templates (as Python triple-quoted strings), CSS (embedded in `<style>` tags within HTML strings), and JavaScript (embedded in `<script>` tags within HTML strings, preferably before `</body>`) must be in this single `.py` file.
|
| 524 |
-
2. **Flask Setup:**
|
| 525 |
-
* Import necessary modules: `os, uuid, json, threading, time, datetime, logging, shutil, socket`.
|
| 526 |
-
* Flask imports: `Flask, request, jsonify, Response, render_template_string, redirect, url_for`.
|
| 527 |
-
* Hugging Face imports: `HfApi, hf_hub_download, delete_files` (from `huggingface_hub`), `RepositoryNotFoundError, HfHubHTTPError` (from `huggingface_hub.utils`).
|
| 528 |
-
* Werkzeug imports: `secure_filename` (from `werkzeug.utils`).
|
| 529 |
-
* The app should get its `PORT` from the `PORT` environment variable.
|
| 530 |
-
* The app should get its unique `APP_ID` from the `APP_ID` environment variable. This `APP_ID` is crucial for namespacing data on Hugging Face.
|
| 531 |
-
* The Hugging Face `REPO_ID` is fixed: "{REPO_ID}".
|
| 532 |
-
* `HF_TOKEN_WRITE` and `HF_TOKEN_READ` should be obtained from environment variables.
|
| 533 |
-
3. **Data Persistence with Hugging Face:**
|
| 534 |
-
* The application must manage its primary data (e.g., blog posts, to-do items) using a local JSON file named `app_data.json` (store this name in a variable like `LOCAL_DATA_FILE`).
|
| 535 |
-
* This `LOCAL_DATA_FILE` must be backed up to and restored from the Hugging Face Hub repository (`{REPO_ID}`).
|
| 536 |
-
* The path for this data file in the Hugging Face repo MUST be `sites_data/{{APP_ID}}/app_data.json` (store this in a variable like `DATA_FILE_PATH_IN_REPO`).
|
| 537 |
-
* Implement the following Python functions for data persistence:
|
| 538 |
-
* `download_file_from_hf_to_local(repo_file_path, local_file_path, retries=3, delay=5)`: Downloads a file from `repo_file_path` on HF to `local_file_path`. Handles 404s by returning `False`. Uses `shutil.move` to place the downloaded file correctly from a temporary download location.
|
| 539 |
-
* `upload_local_file_to_hf(local_file_path, repo_file_path, commit_message=None)`: Uploads `local_file_path` to `repo_file_path` on HF.
|
| 540 |
-
* `load_app_data()`:
|
| 541 |
-
* Tries to load data from `LOCAL_DATA_FILE`.
|
| 542 |
-
* If local fails, calls `download_file_from_hf_to_local` for `DATA_FILE_PATH_IN_REPO`.
|
| 543 |
-
* If download also fails (e.g., 404 on first run for this `APP_ID`), it initializes `app_data` with a default structure (e.g., `{{'items': []}}` or based on user prompt's site type) and then calls `save_app_data()` to create it locally and on HF.
|
| 544 |
-
* Stores data in a global variable (e.g., `app_data`).
|
| 545 |
-
* `save_app_data()`: Saves the global `app_data` to `LOCAL_DATA_FILE` and then calls `upload_local_file_to_hf` to sync to HF.
|
| 546 |
-
* `periodic_app_data_backup()`: A thread function that periodically calls `upload_local_file_to_hf` for `LOCAL_DATA_FILE`.
|
| 547 |
-
* On startup (`if __name__ == '__main__':`), the app must call `load_app_data()` and start the `periodic_app_data_backup` thread if `HF_TOKEN_WRITE` is available.
|
| 548 |
-
4. **File Uploads (if applicable to the user's request):**
|
| 549 |
-
* If the user requests features involving file uploads (e.g., images for posts):
|
| 550 |
-
* Uploaded files must be stored on Hugging Face in the path `sites_data/{{APP_ID}}/uploads/{{filename_in_repo}}`.
|
| 551 |
-
* Implement a helper function like `handle_file_upload(flask_file_storage_object, desired_filename_in_repo=None)`:
|
| 552 |
-
* Takes a Flask `FileStorage` object.
|
| 553 |
-
* Saves it temporarily locally.
|
| 554 |
-
* Uploads it to the constructed HF path using `upload_local_file_to_hf`.
|
| 555 |
-
* Returns the public Hugging Face URL of the uploaded file (e.g., `https://huggingface.co/datasets/{REPO_ID}/resolve/main/sites_data/{{APP_ID}}/uploads/{{filename_in_repo}}`) or an error.
|
| 556 |
-
* Cleans up the local temporary file.
|
| 557 |
-
5. **Logging:**
|
| 558 |
-
* Implement basic logging using the `logging` module. Prefix log messages with the `APP_ID` for clarity: `logging.basicConfig(level=logging.INFO, format=f'%(asctime)s - %(levelname)s - {{APP_ID}} - %(message)s')`.
|
| 559 |
-
6. **Structure:**
|
| 560 |
-
* Start with all necessary imports.
|
| 561 |
-
* Define HF configuration constants/variables (`APP_ID` from env, `REPO_ID`, `HF_TOKEN_WRITE` from env, `HF_TOKEN_READ` from env, `LOCAL_DATA_FILE`, `DATA_FILE_PATH_IN_REPO`, `UPLOADED_FILES_PATH_IN_REPO_BASE`).
|
| 562 |
-
* Define logging setup.
|
| 563 |
-
* Implement all Hugging Face helper functions (`download_file_from_hf_to_local`, `upload_local_file_to_hf`, `load_app_data`, `save_app_data`, `periodic_app_data_backup`, and `handle_file_upload` if needed).
|
| 564 |
-
* Define the Flask `app = Flask(__name__)`.
|
| 565 |
-
* Define a global variable for application data, e.g., `app_data = {{}}`.
|
| 566 |
-
* Define HTML templates as multi-line strings (e.g., `INDEX_HTML_TEMPLATE = """..."""`).
|
| 567 |
-
* Define Flask routes (`@app.route(...)`) as requested by the user, using `render_template_string` and interacting with `app_data` and `save_app_data()`.
|
| 568 |
-
* The main execution block: `if __name__ == '__main__':` should:
|
| 569 |
-
* Get `PORT` from `os.environ.get("PORT", 5001)`.
|
| 570 |
-
* Set `APP_ID = os.environ.get("APP_ID", "default_generated_app_" + str(uuid.uuid4()))`.
|
| 571 |
-
* Call `load_app_data()`.
|
| 572 |
-
* Start the `periodic_app_data_backup` thread.
|
| 573 |
-
* Run the Flask app: `app.run(host='0.0.0.0', port=port, debug=False)`.
|
| 574 |
-
7. **Output Format:** Directly output ONLY the Python code starting with import statements and ending with `app.run(...)`. Do not include any explanatory text, markdown formatting (like ```python), or anything else before or after the Python code itself. Make the site visually appealing and functional.
|
| 575 |
-
|
| 576 |
-
User's request for the website: "{user_prompt}"
|
| 577 |
"""
|
| 578 |
-
|
| 579 |
-
full_prompt = system_instruction # Removed redundant f-string here
|
| 580 |
-
|
| 581 |
-
response = None
|
| 582 |
-
try:
|
| 583 |
-
# model = genai.GenerativeModel('gemini-1.5-flash-latest') # Or your preferred model
|
| 584 |
-
model = genai.GenerativeModel('learnlm-2.0-flash-experimental') # Using the one from original code
|
| 585 |
-
|
| 586 |
-
logging.info("Generating Python app code with AI...")
|
| 587 |
-
response = model.generate_content(full_prompt)
|
| 588 |
-
|
| 589 |
-
generated_text = ""
|
| 590 |
-
if hasattr(response, 'text') and response.text:
|
| 591 |
-
generated_text = response.text
|
| 592 |
-
elif hasattr(response, 'parts') and response.parts: # Check if parts exist
|
| 593 |
-
generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
|
| 594 |
-
|
| 595 |
-
# This resolve logic might be specific or potentially not needed with newer client libraries
|
| 596 |
-
if not generated_text.strip().lower().startswith("import ") and not generated_text.strip().lower().startswith("#"):
|
| 597 |
-
try:
|
| 598 |
-
# response.resolve() # This method might not exist or work as expected.
|
| 599 |
-
# GenAI client usually resolves content automatically.
|
| 600 |
-
# If it's consistently returning partials, the model or parameters might need adjustment.
|
| 601 |
-
if hasattr(response, 'text') and response.text: # Re-check after potential resolve
|
| 602 |
-
generated_text = response.text
|
| 603 |
-
elif hasattr(response, 'parts') and response.parts:
|
| 604 |
-
generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
|
| 605 |
-
except Exception as resolve_e:
|
| 606 |
-
logging.warning(f"response.resolve() failed or did not change output: {resolve_e}")
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
if not generated_text.strip():
|
| 610 |
-
if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
|
| 611 |
-
reason = response.prompt_feedback.block_reason
|
| 612 |
-
raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
|
| 613 |
-
else:
|
| 614 |
-
raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
|
| 615 |
-
|
| 616 |
-
clean_text = generated_text.strip()
|
| 617 |
-
if clean_text.startswith("```python"):
|
| 618 |
-
clean_text = clean_text[9:]
|
| 619 |
-
if clean_text.endswith("```"):
|
| 620 |
-
clean_text = clean_text[:-3]
|
| 621 |
-
generated_text = clean_text.strip()
|
| 622 |
-
elif clean_text.startswith("```"): # More generic markdown block
|
| 623 |
-
clean_text = clean_text[3:]
|
| 624 |
-
if clean_text.endswith("```"):
|
| 625 |
-
clean_text = clean_text[:-3]
|
| 626 |
-
generated_text = clean_text.strip()
|
| 627 |
-
|
| 628 |
-
# A very basic check for Python code
|
| 629 |
-
if not (clean_text.lower().startswith("import ") or clean_text.lower().startswith("#") or "def " in clean_text.lower() or "app = flask(" in clean_text.lower()):
|
| 630 |
-
logging.warning(f"Output might not be Python code. Preview: {clean_text[:300]}")
|
| 631 |
-
# Consider raising an error if it's clearly not Python
|
| 632 |
-
|
| 633 |
-
return generated_text
|
| 634 |
-
|
| 635 |
-
except Exception as e:
|
| 636 |
-
logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
|
| 637 |
-
error_message = str(e)
|
| 638 |
-
# ... (error handling from original code, slightly adapted) ...
|
| 639 |
-
if "API key not valid" in error_message or "API_KEY_INVALID" in error_message:
|
| 640 |
-
raise ValueError("Неверный или отсутствующий GOOGLE_AI_API_KEY.")
|
| 641 |
-
elif "Billing account not found" in error_message or "billing account" in error_message.lower():
|
| 642 |
-
raise ValueError("Проблема с биллингом аккаунта Google Cloud.")
|
| 643 |
-
elif "Could not find model" in error_message:
|
| 644 |
-
raise ValueError(f"Модель не найдена или недоступна.")
|
| 645 |
-
elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
|
| 646 |
-
raise ValueError("Квота запросов Google AI исчерпана. Попробуйте позже.")
|
| 647 |
-
elif ("content has been blocked" in error_message.lower() or
|
| 648 |
-
(response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
|
| 649 |
-
reason = "неизвестна"
|
| 650 |
-
if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
|
| 651 |
-
reason = str(response.prompt_feedback.block_reason) # Make sure it's a string
|
| 652 |
-
elif "safety settings" in error_message.lower():
|
| 653 |
-
reason = "настройки безопасности"
|
| 654 |
-
raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
|
| 655 |
-
else:
|
| 656 |
-
raise ValueError(f"Ошибка при генерации Python-кода приложения: {e}")
|
| 657 |
|
|
|
|
| 658 |
|
| 659 |
@app.route('/')
|
| 660 |
def index():
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
|
| 665 |
@app.route('/generate', methods=['POST'])
|
| 666 |
def handle_generate():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
if 'prompt' not in request.form:
|
| 668 |
return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
|
| 669 |
|
| 670 |
user_prompt = request.form['prompt']
|
| 671 |
|
| 672 |
if not user_prompt or not user_prompt.strip():
|
| 673 |
-
|
| 674 |
|
| 675 |
try:
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
if not python_app_code or not python_app_code.strip():
|
| 679 |
-
return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
|
| 680 |
-
|
| 681 |
-
app_id = str(uuid.uuid4())
|
| 682 |
-
filename = f"{app_id}.py"
|
| 683 |
-
filepath = os.path.join(GENERATED_APPS_DIR, filename)
|
| 684 |
-
|
| 685 |
-
with open(filepath, "w", encoding="utf-8") as f:
|
| 686 |
-
f.write(python_app_code)
|
| 687 |
-
|
| 688 |
-
# Find an available port for the new app
|
| 689 |
-
assigned_port = find_available_port(BASE_PORT_FOR_GENERATED_APPS + len(running_generated_apps))
|
| 690 |
-
|
| 691 |
-
# Prepare environment for the subprocess
|
| 692 |
-
env = os.environ.copy()
|
| 693 |
-
env['PORT'] = str(assigned_port)
|
| 694 |
-
env['APP_ID'] = app_id
|
| 695 |
-
env['REPO_ID'] = REPO_ID # Pass repo_id as well
|
| 696 |
-
if HF_TOKEN_WRITE: env['HF_TOKEN'] = HF_TOKEN_WRITE # For older name compatibility
|
| 697 |
-
if HF_TOKEN_WRITE: env['HF_TOKEN_WRITE'] = HF_TOKEN_WRITE
|
| 698 |
-
if HF_TOKEN_READ: env['HF_TOKEN_READ'] = HF_TOKEN_READ
|
| 699 |
-
elif HF_TOKEN_WRITE: env['HF_TOKEN_READ'] = HF_TOKEN_WRITE # Use write token if read token not set
|
| 700 |
-
|
| 701 |
-
# Launch the generated Python Flask app as a subprocess
|
| 702 |
-
logging.info(f"Attempting to launch generated app {app_id} on port {assigned_port} from {filepath}")
|
| 703 |
-
|
| 704 |
-
# Use sys.executable to ensure it's the same Python interpreter
|
| 705 |
-
# Add current directory to PYTHONPATH for the subprocess to find modules if needed, though single-file apps shouldn't need this.
|
| 706 |
-
# env['PYTHONPATH'] = os.getcwd() + os.pathsep + env.get('PYTHONPATH', '')
|
| 707 |
-
|
| 708 |
-
process = subprocess.Popen([sys.executable, filepath], env=env)
|
| 709 |
-
|
| 710 |
-
# Give it a moment to start (optional, but can help avoid race conditions for immediate access)
|
| 711 |
-
# time.sleep(3) # Consider if this is necessary
|
| 712 |
-
|
| 713 |
-
site_url = f"http://localhost:{assigned_port}/" # Assuming generated app runs on localhost for now
|
| 714 |
-
running_generated_apps[app_id] = {'process': process, 'port': assigned_port, 'url': site_url}
|
| 715 |
-
|
| 716 |
-
logging.info(f"Generated app {app_id} launched. URL: {site_url}")
|
| 717 |
-
return jsonify({"site_url": site_url, "app_id": app_id, "repo_id": REPO_ID})
|
| 718 |
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
return jsonify({"error": str(ve)}), 400
|
| 722 |
-
except RuntimeError as re: # For port finding issues
|
| 723 |
-
logging.error(f"RuntimeError: {re}")
|
| 724 |
-
return jsonify({"error": str(re)}), 500
|
| 725 |
-
except Exception as e:
|
| 726 |
-
logging.error(f"Unexpected error during app generation or launch: {e}", exc_info=True)
|
| 727 |
-
return jsonify({"error": f"Внутренняя ошибка сервера при генерации или запуске приложения: {e}"}), 500
|
| 728 |
|
| 729 |
-
|
| 730 |
-
|
| 731 |
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
logging.error(f"Error terminating app {app_id}: {e}")
|
| 744 |
-
logging.info("All tracked generated applications have been issued termination signals.")
|
| 745 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
|
| 747 |
if __name__ == '__main__':
|
| 748 |
-
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
|
| 751 |
port = int(os.environ.get('PORT', 7860))
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
app.run(host='0.0.0.0', port=port, debug=debug_mode)
|
|
|
|
| 1 |
+
--- START OF FILE app (1) (9).py ---
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import uuid
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
from flask import Flask, request, jsonify, Response, render_template_string, flash, redirect, url_for
|
| 12 |
import google.generativeai as genai
|
| 13 |
+
from huggingface_hub import HfApi, hf_hub_download
|
| 14 |
+
from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
|
| 15 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
# Загружаем переменные окружения из файла .env
|
| 18 |
+
load_dotenv()
|
| 19 |
|
| 20 |
app = Flask(__name__)
|
| 21 |
+
# Устанавливаем секретный ключ для работы flash-сообщений
|
| 22 |
+
app.secret_key = os.getenv("FLASK_SECRET_KEY", "your_unique_secret_key_for_eva_app")
|
| 23 |
|
| 24 |
+
# --- Конфигурация Hugging Face ---
|
| 25 |
+
# ID репозитория на Hugging Face для хранения метаданных сайтов
|
| 26 |
+
REPO_ID = "Kgshop/testsynk"
|
| 27 |
+
# Токены для доступа к Hugging Face (для записи и чтения)
|
| 28 |
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
| 29 |
+
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 30 |
|
| 31 |
+
# Файл, который будет синхронизироваться с Hugging Face. В нем хранятся метаданные всех сгенерированных сайтов.
|
| 32 |
+
DATA_FILE = 'generated_sites_metadata.json'
|
| 33 |
+
SYNC_FILES = [DATA_FILE]
|
| 34 |
|
| 35 |
+
# Каталог для потенциальных временных файлов или статических ассетов (в данной реализации используется минимально)
|
| 36 |
+
GENERATED_SITES_DIR = 'generated_sites'
|
| 37 |
|
| 38 |
+
# Настройки для скачивания/загрузки
|
| 39 |
+
DOWNLOAD_RETRIES = 3
|
| 40 |
+
DOWNLOAD_DELAY = 5
|
| 41 |
+
BACKUP_INTERVAL_SECONDS = 1800 # 30 минут для периодического бэкапа
|
| 42 |
|
| 43 |
+
# Настройка логирования
|
| 44 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 45 |
|
| 46 |
+
# Убедимся, что каталог для сгенерированных сайтов существует (хотя сами сайты теперь не сохраняются как отдельные файлы)
|
| 47 |
+
if not os.path.exists(GENERATED_SITES_DIR):
|
| 48 |
+
os.makedirs(GENERATED_SITES_DIR)
|
| 49 |
+
|
| 50 |
+
# --- Утилитарные функции для работы с Hugging Face (адаптировано из Кода 2) ---
|
| 51 |
+
|
| 52 |
+
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 53 |
+
"""
|
| 54 |
+
Скачивает файлы базы данных с Hugging Face Hub.
|
| 55 |
+
Если файл не найден на HF и локально отсутствует, создает пустой.
|
| 56 |
+
"""
|
| 57 |
+
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 58 |
+
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE не установлены. Скачивание может завершиться неудачей для приватных репозиториев.")
|
| 59 |
+
|
| 60 |
+
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 61 |
+
|
| 62 |
+
files_to_download = [specific_file] if specific_file else SYNC_FILES
|
| 63 |
+
logging.info(f"Попытка скачивания файлов {files_to_download} из репозитория {REPO_ID}...")
|
| 64 |
+
all_successful = True
|
| 65 |
+
|
| 66 |
+
for file_name in files_to_download:
|
| 67 |
+
success = False
|
| 68 |
+
for attempt in range(retries + 1):
|
| 69 |
+
try:
|
| 70 |
+
logging.info(f"Скачивание {file_name} (Попытка {attempt + 1}/{retries + 1})...")
|
| 71 |
+
local_path = hf_hub_download(
|
| 72 |
+
repo_id=REPO_ID,
|
| 73 |
+
filename=file_name,
|
| 74 |
+
repo_type="dataset",
|
| 75 |
+
token=token_to_use,
|
| 76 |
+
local_dir=".",
|
| 77 |
+
local_dir_use_symlinks=False,
|
| 78 |
+
force_download=True,
|
| 79 |
+
resume_download=False
|
| 80 |
+
)
|
| 81 |
+
logging.info(f"Файл {file_name} успешно скачан в {local_path}.")
|
| 82 |
+
success = True
|
| 83 |
+
break
|
| 84 |
+
except RepositoryNotFoundError:
|
| 85 |
+
logging.error(f"Репозиторий {REPO_ID} не ��айден. Скачивание отменено для всех файлов.")
|
| 86 |
+
return False
|
| 87 |
+
except HfHubHTTPError as e:
|
| 88 |
+
if e.response.status_code == 404:
|
| 89 |
+
logging.warning(f"Файл {file_name} не найден в репозитории {REPO_ID} (404). Пропускаем этот файл.")
|
| 90 |
+
if attempt == 0 and not os.path.exists(file_name):
|
| 91 |
+
try:
|
| 92 |
+
if file_name == DATA_FILE:
|
| 93 |
+
with open(file_name, 'w', encoding='utf-8') as f:
|
| 94 |
+
json.dump({}, f) # Создаем пустой JSON-объект для метаданных
|
| 95 |
+
logging.info(f"Создан пустой локальный файл {file_name}, так как он не был найден на HF.")
|
| 96 |
+
except Exception as create_e:
|
| 97 |
+
logging.error(f"Не удалось создать пустой локальный файл {file_name}: {create_e}")
|
| 98 |
+
success = False # Все равно считаем, что для этого файла не удалось
|
| 99 |
+
break
|
| 100 |
+
else:
|
| 101 |
+
logging.error(f"HTTP-ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...")
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logging.error(f"Неожиданная ошибка при скачивании {file_name} (Попытка {attempt + 1}): {e}. Повторная попытка через {delay}с...", exc_info=True)
|
| 104 |
+
|
| 105 |
+
if attempt < retries:
|
| 106 |
+
time.sleep(delay)
|
| 107 |
+
|
| 108 |
+
if not success:
|
| 109 |
+
logging.error(f"Не удалось скачать {file_name} после {retries + 1} попыток.")
|
| 110 |
+
all_successful = False
|
| 111 |
+
|
| 112 |
+
logging.info(f"Процесс скачивания завершен. Общий успех: {all_successful}")
|
| 113 |
+
return all_successful
|
| 114 |
+
|
| 115 |
+
def upload_db_to_hf(specific_file=None):
|
| 116 |
+
"""
|
| 117 |
+
Загружает файлы базы данных на Hugging Face Hub.
|
| 118 |
+
"""
|
| 119 |
+
if not HF_TOKEN_WRITE:
|
| 120 |
+
logging.warning("HF_TOKEN (для записи) не установлен. Пропускаем загрузку на Hugging Face.")
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
api = HfApi()
|
| 125 |
+
files_to_upload = [specific_file] if specific_file else SYNC_FILES
|
| 126 |
+
logging.info(f"Начало загрузки файлов {files_to_upload} в репозиторий HF {REPO_ID}...")
|
| 127 |
+
|
| 128 |
+
for file_name in files_to_upload:
|
| 129 |
+
if os.path.exists(file_name):
|
| 130 |
+
try:
|
| 131 |
+
api.upload_file(
|
| 132 |
+
path_or_fileobj=file_name,
|
| 133 |
+
path_in_repo=file_name,
|
| 134 |
+
repo_id=REPO_ID,
|
| 135 |
+
repo_type="dataset",
|
| 136 |
+
token=HF_TOKEN_WRITE,
|
| 137 |
+
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 138 |
+
)
|
| 139 |
+
logging.info(f"Файл {file_name} успешно загружен на Hugging Face.")
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logging.error(f"Ошибка загрузки файла {file_name} на Hugging Face: {e}")
|
| 142 |
+
else:
|
| 143 |
+
logging.warning(f"Файл {file_name} не найден локально, пропускаем загрузку.")
|
| 144 |
+
logging.info("Загрузка файлов на HF завершена.")
|
| 145 |
+
except Exception as e:
|
| 146 |
+
logging.error(f"Общая ошибка при инициализации или процессе загрузки Hugging Face: {e}", exc_info=True)
|
| 147 |
+
|
| 148 |
+
def periodic_backup():
|
| 149 |
+
"""
|
| 150 |
+
Запускает периодическое резервное копирование данных на Hugging Face.
|
| 151 |
+
"""
|
| 152 |
+
logging.info(f"Настройка периодического резервного копирования каждые {BACKUP_INTERVAL_SECONDS} секунд.")
|
| 153 |
+
while True:
|
| 154 |
+
time.sleep(BACKUP_INTERVAL_SECONDS)
|
| 155 |
+
logging.info("Начало периодического резервного копирования...")
|
| 156 |
+
upload_db_to_hf()
|
| 157 |
+
logging.info("Периодическое резервное копирование завершено.")
|
| 158 |
+
|
| 159 |
+
# --- Функции для сохранения/загрузки метаданных сгенерированных сайтов (адаптировано из Кода 2) ---
|
| 160 |
+
|
| 161 |
+
def load_site_metadata():
|
| 162 |
+
"""
|
| 163 |
+
Загружает метаданные сгенерированных сайтов из DATA_FILE.
|
| 164 |
+
Если файл не найден или поврежден, пытается скачать с Hugging Face.
|
| 165 |
+
"""
|
| 166 |
+
default_data = {}
|
| 167 |
+
try:
|
| 168 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 169 |
+
data = json.load(file)
|
| 170 |
+
logging.info(f"Локальные метаданные сайта успешно загружены из {DATA_FILE}")
|
| 171 |
+
if not isinstance(data, dict):
|
| 172 |
+
logging.warning(f"Локальный файл {DATA_FILE} не является словарем. Попытка скачивания.")
|
| 173 |
+
raise FileNotFoundError # Считаем поврежденным, пытаемся скачать
|
| 174 |
+
return data
|
| 175 |
+
except FileNotFoundError:
|
| 176 |
+
logging.warning(f"Локальный файл {DATA_FILE} не найден. Попытка скачивания с HF.")
|
| 177 |
+
except json.JSONDecodeError:
|
| 178 |
+
logging.error(f"Ошибка декодирования JSON в локальном файле {DATA_FILE}. Файл может быть поврежден. Попытка скачивания.")
|
| 179 |
+
|
| 180 |
+
if download_db_from_hf(specific_file=DATA_FILE):
|
| 181 |
+
try:
|
| 182 |
+
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 183 |
+
data = json.load(file)
|
| 184 |
+
logging.info(f"Метаданные сайта успешно загружены из {DATA_FILE} после скачивания.")
|
| 185 |
+
if not isinstance(data, dict):
|
| 186 |
+
logging.error(f"Скачанный файл {DATA_FILE} не является словарем. Используем по умолчанию.")
|
| 187 |
+
return default_data
|
| 188 |
+
return data
|
| 189 |
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
| 190 |
+
logging.error(f"Ошибка загрузки {DATA_FILE} даже после успешного скачивания: {e}. Используем по умолчанию.")
|
| 191 |
+
return default_data
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logging.error(f"Неизвестная ошибка при загрузке скачанного файла {DATA_FILE}: {e}. Используем по умолчанию.", exc_info=True)
|
| 194 |
+
return default_data
|
| 195 |
+
else:
|
| 196 |
+
logging.error(f"Не удалось скачать {DATA_FILE} с HF после нескольких попыток. Используем пустую структуру данных по умолчанию.")
|
| 197 |
+
if not os.path.exists(DATA_FILE):
|
| 198 |
+
try:
|
| 199 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
| 200 |
+
json.dump(default_data, f)
|
| 201 |
+
logging.info(f"Создан пустой локальный файл {DATA_FILE} после неудачного скачивания.")
|
| 202 |
+
except Exception as create_e:
|
| 203 |
+
logging.error(f"Не удалось создать пустой локальный файл {DATA_FILE}: {create_e}")
|
| 204 |
+
return default_data
|
| 205 |
+
|
| 206 |
+
def save_site_metadata(data):
|
| 207 |
+
"""
|
| 208 |
+
Сохраняет метаданные сгенерированных сайтов в DATA_FILE и загружает на Hugging Face.
|
| 209 |
+
"""
|
| 210 |
+
try:
|
| 211 |
+
if not isinstance(data, dict):
|
| 212 |
+
logging.error("Попытка сохранить недопустимую структуру данных (не словарь) для метаданных сайта. Отмена сохранения.")
|
| 213 |
+
return
|
| 214 |
+
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 215 |
+
json.dump(data, file, ensure_ascii=False, indent=2) # indent=2 для читаемости JSON
|
| 216 |
+
logging.info(f"Метаданные сайта успешно сохранены в {DATA_FILE}")
|
| 217 |
+
upload_db_to_hf(specific_file=DATA_FILE)
|
| 218 |
+
except Exception as e:
|
| 219 |
+
logging.error(f"Ошибка сохранения метаданных сайта в {DATA_FILE}: {e}", exc_info=True)
|
| 220 |
+
|
| 221 |
+
# --- Конфигурация Google Generative AI и промпт ---
|
| 222 |
+
|
| 223 |
+
# Google API ключ для генерации контента
|
| 224 |
+
API_KEY_INTERNAL = os.getenv("GOOGLE_API_KEY")
|
| 225 |
+
|
| 226 |
+
def generate_site_json_from_prompt(user_prompt):
|
| 227 |
+
"""
|
| 228 |
+
Генерирует JSON-объект, описывающий структуру сайта, на основе запроса пользователя,
|
| 229 |
+
используя Google Generative AI.
|
| 230 |
+
"""
|
| 231 |
+
try:
|
| 232 |
+
genai.configure(api_key=API_KEY_INTERNAL)
|
| 233 |
+
except Exception as e:
|
| 234 |
+
logging.error(f"Ошибка настройки GenAI: {e}")
|
| 235 |
+
raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
|
| 236 |
+
|
| 237 |
+
if not user_prompt or not user_prompt.strip():
|
| 238 |
+
raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
|
| 239 |
+
|
| 240 |
+
system_instruction = (
|
| 241 |
+
"Ты экспертный ИИ, который разрабатывает простые, функциональные одностраничные веб-приложения. "
|
| 242 |
+
"Когда пользователь описывает веб-сайт, сгенерируй JSON-объект, который строго соответствует следующей схеме. "
|
| 243 |
+
"Этот JSON-объект будет использоваться Flask-приложением для динамического рендеринга сайта. "
|
| 244 |
+
"Не включай HTML, Markdown или пояснительный текст за пределами JSON. Выводи только JSON-строку. "
|
| 245 |
+
"JSON должен быть валидным и напрямую парсируемым. "
|
| 246 |
+
"Для URL-адресов изображений используй общие сервисы-заполнители, такие как `https://placehold.co/600x400?text=Image` или аналогичные, "
|
| 247 |
+
"либо опиши их текстом, если заполнители не подходят. "
|
| 248 |
+
"Убедись, что любой текстовый контент, содержащий символы новой строки, представлен с помощью `\\n` в JSON-строке."
|
| 249 |
+
"\n\nJSON Schema:\n"
|
| 250 |
+
"```json\n"
|
| 251 |
+
"{\n"
|
| 252 |
+
" \"site_title\": \"string\",\n"
|
| 253 |
+
" \"main_heading\": \"string\",\n"
|
| 254 |
+
" \"tagline\": \"string\",\n"
|
| 255 |
+
" \"sections\": [\n"
|
| 256 |
+
" {\n"
|
| 257 |
+
" \"title\": \"string\",\n"
|
| 258 |
+
" \"content\": \"string\",\n"
|
| 259 |
+
" \"type\": \"text\" // or \"list\", \"contact\", \"image_gallery\"\n"
|
| 260 |
+
" }\n"
|
| 261 |
+
" ],\n"
|
| 262 |
+
" \"data_items\": [ // Это действует как \"база данных\" для простых списков/продуктов/элементов портфолио\n"
|
| 263 |
+
" {\n"
|
| 264 |
+
" \"id\": \"unique_string_or_number\",\n"
|
| 265 |
+
" \"name\": \"string\",\n"
|
| 266 |
+
" \"description\": \"string\",\n"
|
| 267 |
+
" \"price\": \"number (optional)\",\n"
|
| 268 |
+
" \"image_url\": \"string (optional, используй заполнитель или общее изображение)\",\n"
|
| 269 |
+
" \"category\": \"string (optional)\",\n"
|
| 270 |
+
" \"fields\": { \"key\": \"value\" } // произвольные дополнительные поля\n"
|
| 271 |
+
" }\n"
|
| 272 |
+
" ],\n"
|
| 273 |
+
" \"contact_info\": { // Дополнительная контактная информация\n"
|
| 274 |
+
" \"email\": \"string (optional)\",\n"
|
| 275 |
+
" \"phone\": \"string (optional)\",\n"
|
| 276 |
+
" \"address\": \"string (optional)\"\n"
|
| 277 |
+
" },\n"
|
| 278 |
+
" \"footer_text\": \"string (optional)\"\n"
|
| 279 |
+
"}\n"
|
| 280 |
+
"```\n"
|
| 281 |
+
"- `sections`: Массив блоков контента.\n"
|
| 282 |
+
" - `type: \"text\"` для общего текстового контента.\n"
|
| 283 |
+
" - `type: \"list\"`, если нужно отобразить элементы из `data_items`. Убедись, что `data_items` заполнен, если используется этот тип.\n"
|
| 284 |
+
" - `type: \"contact\"`, если это раздел контактов, будет использоваться `contact_info`. В `content` может быть дополнительный текст.\n"
|
| 285 |
+
" - `type: \"image_gallery\"`, если запрошена галерея. Для изображений используй URL-адреса заполнителей. Массив `sections[].images` может быть добавлен, если необходимо.\n"
|
| 286 |
+
"- `data_items`: Массив для простых продуктов, услуг, элементов портфолио и т.д. Они будут отображаться в виде карточек, если присутствует раздел типа 'list'.\n"
|
| 287 |
+
"- Убедись, что все строки в JSON правильно экранированы, особенно символы новой строки (используй `\\n`).\n"
|
| 288 |
+
"- Выводи ТОЛЬКО JSON-код. Не включай никакого пояснительного текста или форматирования Markdown (например, ```json) за пределами JSON."
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
full_prompt = f"{system_instruction}\n\nПользовательский запрос: \"{user_prompt}\""
|
| 292 |
+
|
| 293 |
+
response = None
|
| 294 |
+
try:
|
| 295 |
+
# Используем 'gemini-1.5-flash-latest' для лучшего следования JSON-формату
|
| 296 |
+
model = genai.GenerativeModel('gemini-1.5-flash-latest')
|
| 297 |
+
response = model.generate_content(full_prompt)
|
| 298 |
+
|
| 299 |
+
generated_text = ""
|
| 300 |
+
if hasattr(response, 'text') and response.text:
|
| 301 |
+
generated_text = response.text
|
| 302 |
+
elif response.parts:
|
| 303 |
+
generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
|
| 304 |
+
|
| 305 |
+
if not generated_text.strip():
|
| 306 |
+
if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
|
| 307 |
+
reason = response.prompt_feedback.block_reason
|
| 308 |
+
raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
|
| 309 |
+
else:
|
| 310 |
+
raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
|
| 311 |
|
| 312 |
+
# Удаляем возможное форматирование Markdown (```json...```)
|
| 313 |
+
clean_text = generated_text.strip()
|
| 314 |
+
if clean_text.startswith("```json"):
|
| 315 |
+
clean_text = clean_text[7:]
|
| 316 |
+
if clean_text.endswith("```"):
|
| 317 |
+
clean_text = clean_text[:-3]
|
| 318 |
+
generated_text = clean_text.strip()
|
| 319 |
+
|
| 320 |
+
# Пытаемся распарсить как JSON для валидации
|
| 321 |
+
parsed_json = json.loads(generated_text)
|
| 322 |
+
return parsed_json
|
| 323 |
+
|
| 324 |
+
except json.JSONDecodeError as jde:
|
| 325 |
+
logging.error(f"Вывод ИИ был невалидным JSON: {generated_text[:500]}... Ошибка: {jde}")
|
| 326 |
+
raise ValueError("Модель сгенерировала невалидный JSON. Пожалуйста, попробуйте еще раз или измените запрос.")
|
| 327 |
+
except Exception as e:
|
| 328 |
+
logging.error(f"Ошибка генерации контента с GenAI: {e}", exc_info=True)
|
| 329 |
+
error_message = str(e)
|
| 330 |
+
if "API key not valid" in error_message or "Invalid API key" in error_message:
|
| 331 |
+
raise ValueError("Внутренняя ошибка конфигурации API. Проверьте ключ Google API.")
|
| 332 |
+
elif "Billing account not found" in error_message or "billing account" in error_message.lower():
|
| 333 |
+
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Возможно, аккаунт не настроен или лимит исчерпан.")
|
| 334 |
+
elif "Could not find model" in error_message:
|
| 335 |
+
raise ValueError(f"Модель 'gemini-1.5-flash-latest' не найдена или недоступна. Возможно, используйте 'learnlm-2.0-flash-experimental'.")
|
| 336 |
+
elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
|
| 337 |
+
raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
|
| 338 |
+
elif ("content has been blocked" in error_message.lower() or
|
| 339 |
+
(response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason)):
|
| 340 |
+
reason = "неизвестна"
|
| 341 |
+
if response and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
|
| 342 |
+
reason = response.prompt_feedback.block_reason
|
| 343 |
+
elif "safety settings" in error_message.lower():
|
| 344 |
+
reason = "настройки безопасности"
|
| 345 |
+
raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
|
| 346 |
+
else:
|
| 347 |
+
raise ValueError(f"Ошибка при генерации данных сайта: {e}")
|
| 348 |
|
| 349 |
+
# --- HTML-шаблоны для UI EVA и для сгенерированных сайтов ---
|
| 350 |
|
| 351 |
+
# Основной HTML-шаблон для главной страницы EVA, включающий форму и список сгенерированных сайтов
|
| 352 |
+
index_page_template = """
|
| 353 |
<!DOCTYPE html>
|
| 354 |
<html lang="ru">
|
| 355 |
<head>
|
| 356 |
<meta charset="UTF-8">
|
| 357 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
| 358 |
+
<title>EVA - Генератор Сайтов</title>
|
| 359 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 360 |
<style>
|
| 361 |
:root {
|
| 362 |
--system-gray-100-light: #f2f2f7;
|
|
|
|
| 412 |
--secondary-text-color: var(--system-gray-light-75-light);
|
| 413 |
--tertiary-text-color: var(--system-gray-light-50-light);
|
| 414 |
--border-color: var(--system-separator-light);
|
| 415 |
+
--border-color-opaque: var(--system-separator-opaque-light);
|
| 416 |
--input-bg: var(--system-gray-75-light);
|
| 417 |
--primary-color: var(--system-blue-light);
|
| 418 |
--primary-color-hover: var(--system-blue-light-hover);
|
|
|
|
| 421 |
}
|
| 422 |
|
| 423 |
html {
|
| 424 |
+
height: -webkit-fill-available;
|
| 425 |
}
|
| 426 |
|
| 427 |
body {
|
|
|
|
| 445 |
padding: 25px 30px 30px 30px;
|
| 446 |
border-radius: 24px;
|
| 447 |
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
|
| 448 |
+
max-width: 780px; /* Increased max-width */
|
| 449 |
width: calc(100% - 40px);
|
| 450 |
box-sizing: border-box;
|
| 451 |
margin-top: 30px;
|
|
|
|
| 497 |
}
|
| 498 |
|
| 499 |
textarea#prompt-input:focus {
|
| 500 |
+
border-color: var(--primary-color);
|
| 501 |
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
|
| 502 |
+
outline: none;
|
| 503 |
}
|
| 504 |
|
| 505 |
button#generate-button {
|
|
|
|
| 534 |
}
|
| 535 |
|
| 536 |
.output-header {
|
| 537 |
+
display: flex;
|
| 538 |
+
justify-content: space-between;
|
| 539 |
+
align-items: center;
|
| 540 |
+
margin-bottom: 10px;
|
| 541 |
}
|
| 542 |
|
| 543 |
label#output-label {
|
|
|
|
| 557 |
padding: 5px 8px;
|
| 558 |
border-radius: 6px;
|
| 559 |
transition: background-color 0.2s ease, color 0.2s ease;
|
| 560 |
+
display: none;
|
| 561 |
}
|
| 562 |
|
| 563 |
button#copy-button:hover {
|
| 564 |
background-color: color-mix(in srgb, var(--primary-color) 15%, transparent);
|
| 565 |
}
|
| 566 |
+
|
| 567 |
button#copy-button:active {
|
| 568 |
background-color: color-mix(in srgb, var(--primary-color) 25%, transparent);
|
| 569 |
}
|
| 570 |
+
|
| 571 |
button#copy-button.copied {
|
| 572 |
+
color: #34c759;
|
| 573 |
}
|
| 574 |
@media (prefers-color-scheme: dark) {
|
| 575 |
+
button#copy-button.copied {
|
| 576 |
+
color: #30d158;
|
| 577 |
+
}
|
| 578 |
}
|
| 579 |
|
| 580 |
#output-container {
|
|
|
|
| 592 |
align-items: center;
|
| 593 |
justify-content: center;
|
| 594 |
}
|
| 595 |
+
|
| 596 |
#output-container a {
|
| 597 |
color: var(--primary-color);
|
| 598 |
text-decoration: none;
|
|
|
|
| 603 |
text-decoration: underline;
|
| 604 |
}
|
| 605 |
|
|
|
|
| 606 |
#output-container.loading::before {
|
| 607 |
+
content: "Генерация сайта...";
|
| 608 |
display: block;
|
| 609 |
text-align: center;
|
| 610 |
font-style: italic;
|
|
|
|
| 625 |
50% { opacity: 1; }
|
| 626 |
}
|
| 627 |
|
| 628 |
+
.site-list-section {
|
| 629 |
+
margin-top: 50px;
|
| 630 |
+
border-top: 1px solid var(--border-color);
|
| 631 |
+
padding-top: 30px;
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
.site-list-section h2 {
|
| 635 |
+
font-size: 24px;
|
| 636 |
+
font-weight: 600;
|
| 637 |
+
text-align: center;
|
| 638 |
+
margin-bottom: 25px;
|
| 639 |
+
color: var(--text-color);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.site-cards-grid {
|
| 643 |
+
display: grid;
|
| 644 |
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 645 |
+
gap: 25px;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.site-card {
|
| 649 |
+
background-color: var(--system-gray-75-light);
|
| 650 |
+
border-radius: 16px;
|
| 651 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
| 652 |
+
padding: 20px;
|
| 653 |
+
display: flex;
|
| 654 |
+
flex-direction: column;
|
| 655 |
+
justify-content: space-between;
|
| 656 |
+
border: 1px solid var(--border-color-opaque);
|
| 657 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 658 |
+
}
|
| 659 |
+
.site-card:hover {
|
| 660 |
+
transform: translateY(-3px);
|
| 661 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
| 662 |
+
}
|
| 663 |
+
@media (prefers-color-scheme: dark) {
|
| 664 |
+
.site-card {
|
| 665 |
+
background-color: var(--system-gray-75-dark);
|
| 666 |
+
}
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
|
| 670 |
+
.site-card h3 {
|
| 671 |
+
font-size: 20px;
|
| 672 |
+
font-weight: 600;
|
| 673 |
+
color: var(--primary-color);
|
| 674 |
+
margin-bottom: 10px;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.site-card p {
|
| 678 |
+
font-size: 14px;
|
| 679 |
+
color: var(--secondary-text-color);
|
| 680 |
+
margin-bottom: 8px;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.site-card .actions {
|
| 684 |
+
margin-top: 15px;
|
| 685 |
+
display: flex;
|
| 686 |
+
gap: 10px;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
.site-card .actions a, .site-card .actions button {
|
| 690 |
+
flex: 1;
|
| 691 |
+
padding: 10px 15px;
|
| 692 |
+
border-radius: 10px;
|
| 693 |
+
font-size: 14px;
|
| 694 |
+
font-weight: 500;
|
| 695 |
+
text-align: center;
|
| 696 |
+
text-decoration: none;
|
| 697 |
+
cursor: pointer;
|
| 698 |
+
transition: background-color 0.2s ease, transform 0.1s ease;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.site-card .actions a {
|
| 702 |
+
background-color: var(--primary-color);
|
| 703 |
+
color: white;
|
| 704 |
+
border: none;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.site-card .actions a:hover {
|
| 708 |
+
background-color: var(--primary-color-hover);
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
.site-card .actions button {
|
| 712 |
+
background-color: var(--system-red-light);
|
| 713 |
+
color: white;
|
| 714 |
+
border: none;
|
| 715 |
+
}
|
| 716 |
+
@media (prefers-color-scheme: dark) {
|
| 717 |
+
.site-card .actions button {
|
| 718 |
+
background-color: var(--system-red-dark);
|
| 719 |
+
}
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
.site-card .actions button:hover {
|
| 723 |
+
background-color: color-mix(in srgb, var(--system-red-light) 80%, black);
|
| 724 |
+
}
|
| 725 |
+
@media (prefers-color-scheme: dark) {
|
| 726 |
+
.site-card .actions button:hover {
|
| 727 |
+
background-color: color-mix(in srgb, var(--system-red-dark) 80%, black);
|
| 728 |
+
}
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
.site-card .actions button:active {
|
| 732 |
+
transform: scale(0.98);
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.flash-messages {
|
| 736 |
+
margin-top: 20px;
|
| 737 |
+
padding: 15px 20px;
|
| 738 |
+
border-radius: 12px;
|
| 739 |
+
font-size: 15px;
|
| 740 |
+
font-weight: 500;
|
| 741 |
+
text-align: center;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.flash-messages.success {
|
| 745 |
+
background-color: #d4edda;
|
| 746 |
+
color: #155724;
|
| 747 |
+
border: 1px solid #c3e6cb;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.flash-messages.error {
|
| 751 |
+
background-color: #f8d7da;
|
| 752 |
+
color: #721c24;
|
| 753 |
+
border: 1px solid #f5c6cb;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.flash-messages.warning {
|
| 757 |
+
background-color: #fff3cd;
|
| 758 |
+
color: #856404;
|
| 759 |
+
border: 1px solid #ffeeba;
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
@media (max-width: 768px) {
|
| 764 |
body {
|
| 765 |
padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom);
|
| 766 |
align-items: flex-start;
|
|
|
|
| 774 |
h1 {
|
| 775 |
font-size: 28px;
|
| 776 |
}
|
| 777 |
+
p.subtitle {
|
| 778 |
font-size: 16px;
|
| 779 |
margin-bottom: 25px;
|
| 780 |
}
|
| 781 |
.form-group {
|
| 782 |
margin-bottom: 22px;
|
| 783 |
}
|
| 784 |
+
textarea#prompt-input {
|
| 785 |
padding: 12px 15px;
|
| 786 |
min-height: 100px;
|
| 787 |
+
}
|
| 788 |
button#generate-button {
|
| 789 |
+
padding: 15px;
|
| 790 |
+
font-size: 16px;
|
| 791 |
+
}
|
| 792 |
+
#output-container {
|
| 793 |
+
padding: 15px 18px;
|
| 794 |
+
font-size: 14px;
|
| 795 |
+
min-height: 50px;
|
| 796 |
+
}
|
| 797 |
+
.output-section {
|
| 798 |
+
margin-top: 30px;
|
| 799 |
+
}
|
| 800 |
+
.site-list-section {
|
| 801 |
+
margin-top: 35px;
|
| 802 |
+
padding-top: 25px;
|
| 803 |
+
}
|
| 804 |
+
.site-list-section h2 {
|
| 805 |
+
font-size: 20px;
|
| 806 |
+
margin-bottom: 20px;
|
| 807 |
+
}
|
| 808 |
+
.site-cards-grid {
|
| 809 |
+
grid-template-columns: 1fr; /* Stack columns on small screens */
|
| 810 |
+
}
|
| 811 |
+
.site-card {
|
| 812 |
+
padding: 18px;
|
| 813 |
+
}
|
| 814 |
+
.site-card h3 {
|
| 815 |
+
font-size: 18px;
|
| 816 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
}
|
| 818 |
</style>
|
| 819 |
</head>
|
| 820 |
<body>
|
| 821 |
<div class="container">
|
| 822 |
<h1>EVA</h1>
|
| 823 |
+
<p class="subtitle">Генератор сайтов на базе ИИ</p>
|
| 824 |
+
|
| 825 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 826 |
+
{% if messages %}
|
| 827 |
+
{% for category, message in messages %}
|
| 828 |
+
<div class="flash-messages {{ category }}">{{ message }}</div>
|
| 829 |
+
{% endfor %}
|
| 830 |
+
{% endif %}
|
| 831 |
+
{% endwith %}
|
| 832 |
|
| 833 |
<form id="generate-form">
|
| 834 |
<div class="form-group">
|
| 835 |
+
<label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
|
| 836 |
+
<textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай одностраничный сайт-портфолио для веб-дизайнера по имени Алия, с секциями 'Обо мне', 'Мои работы' и 'Контакты'. Используй современный минималистичный дизайн и добавь несколько примеров работ с описанием и ценой."></textarea>
|
| 837 |
</div>
|
| 838 |
|
| 839 |
+
<button type="submit" id="generate-button">Создать сайт</button>
|
| 840 |
</form>
|
| 841 |
|
| 842 |
<div class="output-section">
|
| 843 |
+
<div class="output-header">
|
| 844 |
+
<label id="output-label">Ссылка на сгенерированный сайт</label>
|
| 845 |
+
<button id="copy-button">Копировать</button>
|
| 846 |
+
</div>
|
| 847 |
<div id="output-container" aria-live="polite">
|
|
|
|
| 848 |
</div>
|
| 849 |
</div>
|
| 850 |
+
|
| 851 |
+
<div class="site-list-section">
|
| 852 |
+
<h2>Ваши сгенерированные сайты</h2>
|
| 853 |
+
{% if generated_sites %}
|
| 854 |
+
<div class="site-cards-grid">
|
| 855 |
+
{% for site_id, site_info in generated_sites.items() %}
|
| 856 |
+
<div class="site-card">
|
| 857 |
+
<h3>{{ site_info.ai_generated_data.site_title | default('Без названия') }}</h3>
|
| 858 |
+
<p><strong>ID:</strong> {{ site_id }}</p>
|
| 859 |
+
<p><strong>Создан:</strong> {{ site_info.timestamp }}</p>
|
| 860 |
+
<p><strong>Основной заголовок:</strong> {{ site_info.ai_generated_data.main_heading | default('N/A') }}</p>
|
| 861 |
+
<div class="actions">
|
| 862 |
+
<a href="{{ url_for('serve_generated_site', site_id=site_id) }}" target="_blank">Открыть</a>
|
| 863 |
+
<form method="POST" action="{{ url_for('delete_site') }}" style="display:inline;">
|
| 864 |
+
<input type="hidden" name="site_id" value="{{ site_id }}">
|
| 865 |
+
<button type="submit" onclick="return confirm('Вы уверены, что хотите удалить этот сайт?');">Удалить</button>
|
| 866 |
+
</form>
|
| 867 |
+
</div>
|
| 868 |
+
</div>
|
| 869 |
+
{% endfor %}
|
| 870 |
+
</div>
|
| 871 |
+
{% else %}
|
| 872 |
+
<p style="text-align: center; color: var(--secondary-text-color);">У вас пока нет сгенерированных сайтов. Начните с создания нового!</p>
|
| 873 |
+
{% endif %}
|
| 874 |
+
</div>
|
| 875 |
+
|
| 876 |
</div>
|
| 877 |
|
| 878 |
<script>
|
|
|
|
| 886 |
event.preventDefault();
|
| 887 |
|
| 888 |
if (!promptInput.value.trim()) {
|
| 889 |
+
showError("Пожалуйста, опишите сайт, который вы хотите создать.");
|
| 890 |
return;
|
| 891 |
}
|
| 892 |
|
|
|
|
| 894 |
|
| 895 |
generateButton.disabled = true;
|
| 896 |
generateButton.textContent = 'Генерация...';
|
| 897 |
+
outputContainer.innerHTML = '';
|
| 898 |
outputContainer.classList.add('loading');
|
| 899 |
outputContainer.classList.remove('error');
|
| 900 |
copyButton.style.display = 'none';
|
|
|
|
| 916 |
if (result.site_url) {
|
| 917 |
const link = document.createElement('a');
|
| 918 |
link.href = result.site_url;
|
| 919 |
+
link.textContent = "Открыть сгенерированный сайт";
|
| 920 |
+
link.target = "_blank";
|
| 921 |
+
outputContainer.innerHTML = '';
|
| 922 |
outputContainer.appendChild(link);
|
| 923 |
copyButton.style.display = 'block';
|
| 924 |
+
copyButton.dataset.copyText = window.location.origin + result.site_url;
|
| 925 |
+
// Перезагрузка страницы для отображения нового сайта в списке
|
| 926 |
+
setTimeout(() => {
|
| 927 |
+
window.location.reload();
|
| 928 |
+
}, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 929 |
} else if (result.error) {
|
| 930 |
showError(result.error);
|
| 931 |
} else {
|
| 932 |
+
showError("Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
|
| 933 |
}
|
| 934 |
|
| 935 |
} catch (error) {
|
| 936 |
console.error("Fetch Error:", error);
|
| 937 |
showError(`Ошибка: ${error.message}`);
|
| 938 |
+
copyButton.style.display = 'none';
|
| 939 |
} finally {
|
| 940 |
generateButton.disabled = false;
|
| 941 |
+
generateButton.textContent = 'Создать сайт';
|
| 942 |
outputContainer.classList.remove('loading');
|
| 943 |
}
|
| 944 |
});
|
|
|
|
| 951 |
copyButton.textContent = 'Скопировано!';
|
| 952 |
copyButton.classList.add('copied');
|
| 953 |
setTimeout(() => {
|
| 954 |
+
copyButton.textContent = 'Копировать';
|
| 955 |
+
copyButton.classList.remove('copied');
|
| 956 |
}, 1500);
|
| 957 |
}).catch(err => {
|
| 958 |
console.error('Ошибка копирования: ', err);
|
| 959 |
copyButton.textContent = 'Ошибка';
|
| 960 |
+
setTimeout(() => {
|
| 961 |
+
copyButton.textContent = 'Копировать';
|
| 962 |
}, 1500);
|
| 963 |
});
|
| 964 |
});
|
| 965 |
|
| 966 |
function showError(message) {
|
| 967 |
+
outputContainer.innerHTML = '';
|
| 968 |
+
const errorMessageElement = document.createElement('span');
|
| 969 |
+
errorMessageElement.textContent = message;
|
| 970 |
+
outputContainer.appendChild(errorMessageElement);
|
| 971 |
+
outputContainer.classList.add('error');
|
| 972 |
+
outputContainer.classList.remove('loading');
|
| 973 |
+
copyButton.style.display = 'none';
|
| 974 |
}
|
| 975 |
+
|
| 976 |
</script>
|
| 977 |
</body>
|
| 978 |
</html>
|
| 979 |
"""
|
| 980 |
|
| 981 |
+
# Шаблон для рендеринга динамически сгенерированных сайтов
|
| 982 |
+
dynamic_site_template = """
|
| 983 |
+
<!DOCTYPE html>
|
| 984 |
+
<html lang="ru">
|
| 985 |
+
<head>
|
| 986 |
+
<meta charset="UTF-8">
|
| 987 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 988 |
+
<title>{{ site_data.site_title | default('Сгенерированный Сайт') }}</title>
|
| 989 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
| 990 |
+
<style>
|
| 991 |
+
body { font-family: 'Inter', sans-serif; margin: 0; padding: 0; background-color: #f4f7f6; color: #333; line-height: 1.6; }
|
| 992 |
+
.container { max-width: 900px; margin: 30px auto; background-color: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); overflow: hidden; }
|
| 993 |
+
header { background-color: #007bff; color: white; padding: 40px 20px; text-align: center; }
|
| 994 |
+
header h1 { margin: 0; font-size: 2.8em; font-weight: 700; }
|
| 995 |
+
header p { font-size: 1.2em; opacity: 0.9; margin-top: 10px; }
|
| 996 |
+
.section-content { padding: 30px; border-bottom: 1px solid #eee; }
|
| 997 |
+
.section-content:last-of-type { border-bottom: none; }
|
| 998 |
+
.section-content h2 { color: #007bff; font-size: 2em; margin-bottom: 20px; text-align: center; }
|
| 999 |
+
.text-content p { margin-bottom: 15px; font-size: 1.1em; line-height: 1.8; }
|
| 1000 |
+
.list-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-top: 20px; }
|
| 1001 |
+
.list-item { background-color: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
|
| 1002 |
+
.list-item img { max-width: 100%; height: 180px; object-fit: contain; border-radius: 5px; margin-bottom: 15px; background-color: white; padding: 5px; border: 1px solid #f0f0f0;}
|
| 1003 |
+
.list-item h3 { color: #333; margin-top: 0; font-size: 1.4em; margin-bottom: 10px; }
|
| 1004 |
+
.list-item p { font-size: 0.9em; color: #666; margin-bottom: 10px; }
|
| 1005 |
+
.list-item .price { font-size: 1.2em; font-weight: 600; color: #28a745; margin-top: 10px; }
|
| 1006 |
+
.contact-section p { font-size: 1.1em; margin-bottom: 10px; }
|
| 1007 |
+
.contact-section a { color: #007bff; text-decoration: none; }
|
| 1008 |
+
.contact-section a:hover { text-decoration: underline; }
|
| 1009 |
+
footer { background-color: #333; color: white; text-align: center; padding: 20px; font-size: 0.9em; margin-top: 20px; }
|
| 1010 |
+
@media (max-width: 768px) {
|
| 1011 |
+
header h1 { font-size: 2em; }
|
| 1012 |
+
header p { font-size: 1em; }
|
| 1013 |
+
.section-content { padding: 20px; }
|
| 1014 |
+
.section-content h2 { font-size: 1.6em; }
|
| 1015 |
+
}
|
| 1016 |
+
</style>
|
| 1017 |
+
</head>
|
| 1018 |
+
<body>
|
| 1019 |
+
<div class="container">
|
| 1020 |
+
<header>
|
| 1021 |
+
<h1>{{ site_data.main_heading | default('Добро пожаловать!') }}</h1>
|
| 1022 |
+
<p>{{ site_data.tagline | default('Ваш сгенерированный сайт готов.') }}</p>
|
| 1023 |
+
</header>
|
| 1024 |
+
|
| 1025 |
+
{% for section in site_data.sections %}
|
| 1026 |
+
<div class="section-content">
|
| 1027 |
+
<h2>{{ section.title }}</h2>
|
| 1028 |
+
{% if section.type == 'text' %}
|
| 1029 |
+
<div class="text-content">
|
| 1030 |
+
<p>{{ section.content | replace('\\n', '<br>') | safe }}</p>
|
| 1031 |
+
</div>
|
| 1032 |
+
{% elif section.type == 'list' and site_data.data_items %}
|
| 1033 |
+
<div class="list-grid">
|
| 1034 |
+
{% for item in site_data.data_items %}
|
| 1035 |
+
<div class="list-item">
|
| 1036 |
+
{% if item.image_url %}<img src="{{ item.image_url }}" alt="{{ item.name }}">{% endif %}
|
| 1037 |
+
<h3>{{ item.name }}</h3>
|
| 1038 |
+
<p>{{ item.description | default('') | replace('\\n', '<br>') | safe }}</p>
|
| 1039 |
+
{% if item.price %}<div class="price">{{ "%.2f"|format(item.price) }}</div>{% endif %}
|
| 1040 |
+
{% if item.category %}<p style="font-size: 0.8em; color: #999;">Категория: {{ item.category }}</p>{% endif %}
|
| 1041 |
+
{% for key, value in item.fields.items() %}
|
| 1042 |
+
<p style="font-size: 0.85em; color: #555;"><strong>{{ key|capitalize }}:</strong> {{ value }}</p>
|
| 1043 |
+
{% endfor %}
|
| 1044 |
+
</div>
|
| 1045 |
+
{% endfor %}
|
| 1046 |
+
</div>
|
| 1047 |
+
{% elif section.type == 'contact' and site_data.contact_info %}
|
| 1048 |
+
<div class="contact-section text-content">
|
| 1049 |
+
{% if site_data.contact_info.email %}<p><strong>Email:</strong> <a href="mailto:{{ site_data.contact_info.email }}">{{ site_data.contact_info.email }}</a></p>{% endif %}
|
| 1050 |
+
{% if site_data.contact_info.phone %}<p><strong>Телефон:</strong> <a href="tel:{{ site_data.contact_info.phone }}">{{ site_data.contact_info.phone }}</a></p>{% endif %}
|
| 1051 |
+
{% if site_data.contact_info.address %}<p><strong>Адрес:</strong> {{ site_data.contact_info.address }}</p>{% endif %}
|
| 1052 |
+
<p>{{ section.content | default('') | replace('\\n', '<br>') | safe }}</p>
|
| 1053 |
+
</div>
|
| 1054 |
+
{% elif section.type == 'image_gallery' %}
|
| 1055 |
+
<div class="list-grid">
|
| 1056 |
+
{% if section.images %} {# Assuming images can be passed directly in the section object for gallery #}
|
| 1057 |
+
{% for image_url in section.images %}
|
| 1058 |
+
<div class="list-item">
|
| 1059 |
+
<img src="{{ image_url }}" alt="Галерея">
|
| 1060 |
+
</div>
|
| 1061 |
+
{% endfor %}
|
| 1062 |
+
{% else %}
|
| 1063 |
+
<p style="text-align: center; color: #999;">Галерея изображений пока пуста.</p>
|
| 1064 |
+
{% endif %}
|
| 1065 |
+
</div>
|
| 1066 |
+
<p>{{ section.content | default('') | replace('\\n', '<br>') | safe }}</p>
|
| 1067 |
+
{% endif %}
|
| 1068 |
+
</div>
|
| 1069 |
+
{% endfor %}
|
| 1070 |
|
| 1071 |
+
<footer>
|
| 1072 |
+
<p>{{ site_data.footer_text | default(site_data.site_title + ' © ' + now.year|string + '. Все права защищены.') }}</p>
|
| 1073 |
+
</footer>
|
| 1074 |
+
</div>
|
| 1075 |
+
</body>
|
| 1076 |
+
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
|
| 1079 |
+
# --- Flask Маршруты ---
|
| 1080 |
|
| 1081 |
@app.route('/')
|
| 1082 |
def index():
|
| 1083 |
+
"""
|
| 1084 |
+
Главная страница EVA, отображает форму генерации и список сгенерированных сайтов.
|
| 1085 |
+
"""
|
| 1086 |
+
generated_sites = load_site_metadata()
|
| 1087 |
+
# Сортируем сайты по времени создания (новые сверху) для удобства отображения
|
| 1088 |
+
sorted_sites = dict(sorted(generated_sites.items(), key=lambda item: item[1].get('timestamp', ''), reverse=True))
|
| 1089 |
+
return render_template_string(index_page_template, generated_sites=sorted_sites)
|
| 1090 |
|
| 1091 |
@app.route('/generate', methods=['POST'])
|
| 1092 |
def handle_generate():
|
| 1093 |
+
"""
|
| 1094 |
+
Обрабатывает запрос на генерацию нового сайта.
|
| 1095 |
+
Вызывает AI для получения JSON-описания сайта, сохраняет его и возвращает ссылку.
|
| 1096 |
+
"""
|
| 1097 |
if 'prompt' not in request.form:
|
| 1098 |
return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
|
| 1099 |
|
| 1100 |
user_prompt = request.form['prompt']
|
| 1101 |
|
| 1102 |
if not user_prompt or not user_prompt.strip():
|
| 1103 |
+
return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
|
| 1104 |
|
| 1105 |
try:
|
| 1106 |
+
# Получаем JSON-структуру данных сайта от AI
|
| 1107 |
+
site_data_json = generate_site_json_from_prompt(user_prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
|
| 1109 |
+
if not site_data_json:
|
| 1110 |
+
return jsonify({"error": "Сгенерированные данные сайта пусты."}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
|
| 1112 |
+
site_id = str(uuid.uuid4()) # Генерируем уникальный ID для нового сайта
|
| 1113 |
+
generated_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 1114 |
|
| 1115 |
+
# Загружаем текущие метаданные, добавляем новый сайт и сохраняем
|
| 1116 |
+
site_metadata = load_site_metadata()
|
| 1117 |
+
site_metadata[site_id] = {
|
| 1118 |
+
"timestamp": generated_at,
|
| 1119 |
+
"ai_generated_data": site_data_json
|
| 1120 |
+
}
|
| 1121 |
+
save_site_metadata(site_metadata)
|
| 1122 |
+
|
| 1123 |
+
# Формируем URL для доступа к сгенерированному сайту
|
| 1124 |
+
site_url = url_for('serve_generated_site', site_id=site_id)
|
| 1125 |
+
return jsonify({"site_url": site_url})
|
|
|
|
|
|
|
| 1126 |
|
| 1127 |
+
except ValueError as ve:
|
| 1128 |
+
logging.error(f"Ошибка генерации (ValueError): {ve}")
|
| 1129 |
+
return jsonify({"error": str(ve)}), 400
|
| 1130 |
+
except Exception as e:
|
| 1131 |
+
logging.error(f"Неожиданная ошибка во время генерации сайта: {e}", exc_info=True)
|
| 1132 |
+
return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
|
| 1133 |
+
|
| 1134 |
+
@app.route('/generated_site/<site_id>')
|
| 1135 |
+
def serve_generated_site(site_id):
|
| 1136 |
+
"""
|
| 1137 |
+
Отображает динамически сгенерированный сайт по его ID.
|
| 1138 |
+
Загружает JSON-данные сайта и рендерит их с помощью предопределенного шаблона.
|
| 1139 |
+
"""
|
| 1140 |
+
site_metadata = load_site_metadata()
|
| 1141 |
+
site_info = site_metadata.get(site_id)
|
| 1142 |
+
|
| 1143 |
+
if not site_info:
|
| 1144 |
+
flash(f"Сайт с ID '{site_id}' не найден.", 'error')
|
| 1145 |
+
return redirect(url_for('index'))
|
| 1146 |
+
|
| 1147 |
+
site_data = site_info.get('ai_generated_data')
|
| 1148 |
+
if not site_data:
|
| 1149 |
+
flash(f"Данные для сайта с ID '{site_id}' повреждены.", 'error')
|
| 1150 |
+
return redirect(url_for('index'))
|
| 1151 |
+
|
| 1152 |
+
# Передаем объект datetime для использования года в футере
|
| 1153 |
+
return render_template_string(dynamic_site_template, site_data=site_data, now=datetime.now())
|
| 1154 |
+
|
| 1155 |
+
@app.route('/delete_site', methods=['POST'])
|
| 1156 |
+
def delete_site():
|
| 1157 |
+
"""
|
| 1158 |
+
Удаляет сгенерированный сайт по его ID.
|
| 1159 |
+
"""
|
| 1160 |
+
site_id_to_delete = request.form.get('site_id')
|
| 1161 |
+
if not site_id_to_delete:
|
| 1162 |
+
flash("ID сайта для удаления не предоставлен.", 'error')
|
| 1163 |
+
return redirect(url_for('index'))
|
| 1164 |
+
|
| 1165 |
+
site_metadata = load_site_metadata()
|
| 1166 |
+
if site_id_to_delete in site_metadata:
|
| 1167 |
+
del site_metadata[site_id_to_delete]
|
| 1168 |
+
save_site_metadata(site_metadata) # Сохраняем изменения и синхронизируем с HF
|
| 1169 |
+
flash(f"Сайт с ID '{site_id_to_delete}' успешно удален.", 'success')
|
| 1170 |
+
else:
|
| 1171 |
+
flash(f"Сайт с ID '{site_id_to_delete}' не найден.", 'warning')
|
| 1172 |
+
|
| 1173 |
+
return redirect(url_for('index'))
|
| 1174 |
+
|
| 1175 |
+
# --- Инициализация и запуск приложения ---
|
| 1176 |
|
| 1177 |
if __name__ == '__main__':
|
| 1178 |
+
logging.info("Приложение запускается. Выполняется первоначальная загрузка/скачивание данных...")
|
| 1179 |
+
download_db_from_hf() # Попытка первоначального скачивания файла метаданных
|
| 1180 |
+
load_site_metadata() # Загружаем его (или создаем по умолчанию, если не найден/скачан)
|
| 1181 |
+
logging.info("Первоначальная загрузка данных завершена.")
|
| 1182 |
+
|
| 1183 |
+
if API_KEY_INTERNAL is None:
|
| 1184 |
+
logging.error("Переменная окружения GOOGLE_API_KEY не установлена. Генерация AI будет завершаться ошибкой.")
|
| 1185 |
+
flash("Внимание: Google AI API ключ не настроен. Генерация сайтов будет недоступна.", "warning")
|
| 1186 |
+
|
| 1187 |
+
if HF_TOKEN_WRITE:
|
| 1188 |
+
backup_thread = threading.Thread(target=periodic_backup, daemon=True)
|
| 1189 |
+
backup_thread.start()
|
| 1190 |
+
logging.info("Поток периодического резервного копирования запущен.")
|
| 1191 |
+
else:
|
| 1192 |
+
logging.warning("Периодическое резервное копирование НЕ будет выполняться (HF_TOKEN для записи не установлен).")
|
| 1193 |
|
| 1194 |
port = int(os.environ.get('PORT', 7860))
|
| 1195 |
+
logging.info(f"Запуск Flask-приложения на хосте 0.0.0.0 и порту {port}")
|
| 1196 |
+
app.run(debug=False, host='0.0.0.0', port=port)
|
|
|