Kgshop commited on
Commit
1798d58
·
verified ·
1 Parent(s): 72c3aff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +336 -391
app.py CHANGED
@@ -4,504 +4,449 @@ import json
4
  import logging
5
  import threading
6
  import time
7
- import socket
8
  import subprocess
 
9
  import atexit
10
  from datetime import datetime
11
- from flask import Flask, request, jsonify, Response, render_template_string
 
 
12
  import google.generativeai as genai
13
  from huggingface_hub import HfApi, hf_hub_download
14
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
15
  import requests
16
- from dotenv import load_dotenv
17
-
18
- # --- Конфигурация и инициализация ---
19
 
 
20
  load_dotenv()
 
 
21
  app = Flask(__name__)
 
22
 
23
- # --- Константы из Кода 1 ---
24
- API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Ваш ключ Google AI
25
- GENERATED_APPS_DIR = 'generated_apps'
26
 
27
- # --- Константы из Кода 2 (адаптированные) ---
28
- DATA_FILE = 'running_apps.json'
29
- SYNC_FILES = [DATA_FILE]
30
- REPO_ID = "Kgshop/testsynk" # <-- ВАШ НОВЫЙ РЕПОЗИТОРИЙ
31
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
32
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
33
- DOWNLOAD_RETRIES = 3
34
- DOWNLOAD_DELAY = 5
35
 
36
- # --- Глобальные переменные для управления состоянием ---
37
- RUNNING_APPS = {} # { "uuid": {"process": Popen_object, "port": 12345, "prompt": "..."} }
 
 
38
 
39
- # --- Настройка логирования ---
40
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
41
 
42
- # --- Создание необходимых директорий ---
43
- if not os.path.exists(GENERATED_APPS_DIR):
44
- os.makedirs(GENERATED_APPS_DIR)
45
-
46
-
47
- # --- HTML-шаблон для главной страницы (из Кода 1) ---
48
- html_template = """
49
- <!DOCTYPE html>
50
- <html lang="ru">
51
- <head>
52
- <meta charset="UTF-8">
53
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
54
- <title>EVA - Генератор Сайтов</title>
55
- <style>
56
- :root {
57
- --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff;
58
- --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c;
59
- --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff;
60
- --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29);
61
- --system-separator-opaque-light: #d1d1d6; --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e;
62
- --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7;
63
- --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366;
64
- --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a;
65
- --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
66
- }
67
- @media (prefers-color-scheme: dark) {
68
- :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark);
69
- --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark);
70
- --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark);
71
- --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
72
- @media (prefers-color-scheme: light) {
73
- :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light);
74
- --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light);
75
- --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light);
76
- --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
77
- html { height: -webkit-fill-available; }
78
- body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0;
79
- padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex;
80
- justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45;
81
- -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
82
- .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
83
- max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
84
- h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
85
- p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
86
- .form-group { margin-bottom: 28px; }
87
- label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
88
- textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px;
89
- background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease;
90
- font-family: inherit; resize: vertical; min-height: 120px; }
91
- textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
92
- button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px;
93
- font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
94
- button#generate-button:hover { background-color: var(--primary-color-hover); }
95
- button#generate-button:active { transform: scale(0.98); }
96
- button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
97
- .output-section { margin-top: 35px; }
98
- .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
99
- label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
100
- button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer;
101
- padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; }
102
- button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); }
103
- button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); }
104
- button#copy-button.copied { color: #34c759; }
105
- @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
106
- #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color);
107
- word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease;
108
- display: flex; align-items: center; justify-content: center; }
109
- #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }
110
- #output-container a:hover { text-decoration: underline; }
111
- #output-container.loading::before { content: "Генерация приложения..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
112
- #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
113
- background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
114
- @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
115
- @media (max-width: 620px) {
116
- body { padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom); align-items: flex-start; }
117
- .container { padding: 20px 20px 25px 20px; margin-top: 15px; border-radius: 20px; width: calc(100% - 30px); }
118
- h1 { font-size: 28px; } p.subtitle { font-size: 16px; margin-bottom: 25px; } .form-group { margin-bottom: 22px; }
119
- textarea#prompt-input { padding: 12px 15px; min-height: 100px; } button#generate-button { padding: 15px; font-size: 16px; }
120
- #output-container { padding: 15px 18px; font-size: 14px; min-height: 50px; } .output-section { margin-top: 30px; } }
121
- </style>
122
- </head>
123
- <body>
124
- <div class="container">
125
- <h1>EVA</h1>
126
- <p class="subtitle">Генератор Python сайтов на базе ИИ</p>
127
- <form id="generate-form">
128
- <div class="form-group">
129
- <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
130
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай одностраничный сайт-портфолио для веб-дизайнера Алии. Нужны секции 'Обо мне', 'Работы' и 'Контакты'. Используй современный минималистичный дизайн с темной темой."></textarea>
131
- </div>
132
- <button type="submit" id="generate-button">Создать сайт</button>
133
- </form>
134
- <div class="output-section">
135
- <div class="output-header">
136
- <label id="output-label">Ссылка на сайт</label>
137
- <button id="copy-button">Копировать</button>
138
- </div>
139
- <div id="output-container" aria-live="polite"></div>
140
- </div>
141
- </div>
142
- <script>
143
- const form = document.getElementById('generate-form');
144
- const promptInput = document.getElementById('prompt-input');
145
- const outputContainer = document.getElementById('output-container');
146
- const generateButton = document.getElementById('generate-button');
147
- const copyButton = document.getElementById('copy-button');
148
-
149
- form.addEventListener('submit', async (event) => {
150
- event.preventDefault();
151
- if (!promptInput.value.trim()) { showError("Пожалуйста, опишите сайт, который вы хотите создать."); return; }
152
- const formData = new FormData(form);
153
- generateButton.disabled = true; generateButton.textContent = 'Генерация...';
154
- outputContainer.innerHTML = ''; outputContainer.classList.add('loading'); outputContainer.classList.remove('error');
155
- copyButton.style.display = 'none'; copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied');
156
-
157
- try {
158
- const response = await fetch('/generate', { method: 'POST', body: formData });
159
- const result = await response.json();
160
- if (!response.ok) { throw new Error(result.error || `Ошибка сервера: ${response.status}`); }
161
-
162
- if (result.site_url) {
163
- const link = document.createElement('a');
164
- link.href = result.site_url;
165
- link.textContent = "Открыть сгенерированное приложение";
166
- link.target = "_blank";
167
- outputContainer.innerHTML = '';
168
- outputContainer.appendChild(link);
169
- copyButton.style.display = 'block';
170
- copyButton.dataset.copyText = window.location.origin + result.site_url;
171
- } else {
172
- showError(result.error || "Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
173
- }
174
- } catch (error) {
175
- console.error("Fetch Error:", error);
176
- showError(`Ошибка: ${error.message}`);
177
- copyButton.style.display = 'none';
178
- } finally {
179
- generateButton.disabled = false;
180
- generateButton.textContent = 'Создать сайт';
181
- outputContainer.classList.remove('loading');
182
- }
183
- });
184
- copyButton.addEventListener('click', () => {
185
- const textToCopy = copyButton.dataset.copyText;
186
- if (!textToCopy) return;
187
- navigator.clipboard.writeText(textToCopy).then(() => {
188
- copyButton.textContent = 'Скопировано!'; copyButton.classList.add('copied');
189
- setTimeout(() => { copyButton.textContent = 'Копировать'; copyButton.classList.remove('copied'); }, 1500);
190
- }).catch(err => {
191
- console.error('Ошибка копирования: ', err);
192
- copyButton.textContent = 'Ошибка';
193
- setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
194
- });
195
- });
196
- function showError(message) {
197
- outputContainer.innerHTML = '';
198
- const errorMessageElement = document.createElement('span');
199
- errorMessageElement.textContent = message;
200
- outputContainer.appendChild(errorMessageElement);
201
- outputContainer.classList.add('error');
202
- outputContainer.classList.remove('loading');
203
- copyButton.style.display = 'none';
204
- }
205
- </script>
206
- </body>
207
- </html>
208
- """
209
 
210
- # --- Функции для работы с Hugging Face (из Кода 2) ---
211
 
212
- def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
213
- token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
214
- if not token_to_use:
215
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
216
-
217
  files_to_download = [specific_file] if specific_file else SYNC_FILES
218
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
219
-
 
220
  for file_name in files_to_download:
 
221
  for attempt in range(retries + 1):
222
  try:
 
223
  hf_hub_download(
224
  repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
225
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
226
  )
227
  logging.info(f"Successfully downloaded {file_name}.")
228
- return True
 
 
 
 
229
  except HfHubHTTPError as e:
230
  if e.response.status_code == 404:
231
- logging.warning(f"File {file_name} not found in repo {REPO_ID}. Skipping.")
232
- return False
233
- logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...")
 
 
 
 
 
234
  except Exception as e:
235
- logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
236
- if attempt < retries: time.sleep(delay)
237
- logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
238
- return False
 
 
 
239
 
240
  def upload_db_to_hf(specific_file=None):
241
  if not HF_TOKEN_WRITE:
242
- logging.warning("HF_TOKEN (for writing) not set. Skipping upload.")
243
  return
244
  try:
245
  api = HfApi()
246
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
 
247
  for file_name in files_to_upload:
248
  if os.path.exists(file_name):
249
  api.upload_file(
250
  path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
251
  repo_type="dataset", token=HF_TOKEN_WRITE,
252
- commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
253
  )
254
  logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
 
 
255
  except Exception as e:
256
- logging.error(f"Error uploading file to Hugging Face: {e}", exc_info=True)
257
 
258
  def periodic_backup():
259
  while True:
260
- time.sleep(1800) # 30 минут
261
  logging.info("Starting periodic backup...")
262
- save_data(RUNNING_APPS)
263
- logging.info("Periodic backup finished.")
264
-
265
- # --- Функции управления данными (адаптированные из Кода 2) ---
266
 
267
  def load_data():
268
- if os.path.exists(DATA_FILE):
269
- try:
270
- with open(DATA_FILE, 'r', encoding='utf-8') as f:
271
- return json.load(f)
272
- except json.JSONDecodeError:
273
- logging.error(f"Could not decode {DATA_FILE}. It might be corrupted.")
274
- return {}
275
- return {}
276
-
277
- def save_data(data):
278
- # Сохраняем только метаданные, а не объекты Popen
279
- data_to_save = {}
280
- for site_uuid, details in data.items():
281
- data_to_save[site_uuid] = {
282
- "port": details.get("port"),
283
- "prompt": details.get("prompt"),
284
- "pid": details.get("process").pid if details.get("process") else None,
285
- "filepath": details.get("filepath")
286
- }
287
  try:
288
- with open(DATA_FILE, 'w', encoding='utf-8') as f:
289
- json.dump(data_to_save, f, ensure_ascii=False, indent=4)
290
- logging.info(f"Data successfully saved to {DATA_FILE}")
291
- # Запускаем загрузку в HF в отдельном потоке, чтобы не блокировать основной
292
- threading.Thread(target=upload_db_to_hf, args=(DATA_FILE,)).start()
293
- except Exception as e:
294
- logging.error(f"Error saving data to {DATA_FILE}: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
 
 
 
 
 
 
 
 
 
296
 
297
- # --- Основная логика генерации и управления приложениями ---
298
 
299
- def generate_app_code_from_prompt(user_prompt):
300
- """
301
- Генерирует код Python Flask приложения на основе запроса пользователя.
302
- """
303
  try:
304
  genai.configure(api_key=API_KEY_INTERNAL)
305
  except Exception as e:
306
- logging.error(f"Error configuring GenAI: {e}")
307
- raise ValueError(f"Не удалось настроить Google AI: {e}")
308
 
309
  if not user_prompt or not user_prompt.strip():
310
- raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
311
 
312
- # !ВАЖНО! Новый системный промпт для генерации Python кода
313
  system_instruction = (
314
- "You are an expert Python Flask developer. Your task is to generate a complete, single-file Flask application "
315
- "based on the user's request. The entire application must be contained in a single Python script.\n"
316
- "Follow these rules strictly:\n"
317
- "1. The script must import all necessary libraries, like `Flask`, `render_template_string`, and `os`.\n"
318
- "2. All HTML, CSS, and JavaScript must be embedded within Python multiline strings.\n"
319
- "3. Use `render_template_string` to serve the HTML content.\n"
320
- "4. The application MUST get its port from an environment variable named 'PORT'. Use `os.environ.get('PORT', 5000)`.\n"
321
- "5. The application must run on host '0.0.0.0' to be accessible within a containerized environment.\n"
322
- "6. The script must be directly runnable using `if __name__ == '__main__':`.\n"
323
- "7. Do NOT use any external files, databases, or libraries that are not standard (like flask, os, json).\n"
324
- "8. The generated code must be visually appealing and functional according to the user's request.\n"
325
- "9. CRITICAL: Directly output ONLY the raw Python code. Do not include any explanatory text, markdown formatting (like ```python), or anything else before `import` or after the last line of code."
 
 
 
 
 
 
326
  )
327
 
328
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
329
 
330
  try:
331
- model = genai.GenerativeModel('gemini-1.5-flash-latest')
332
  response = model.generate_content(full_prompt)
333
 
334
- generated_text = response.text
335
- if not generated_text or not generated_text.strip().startswith("import"):
336
- raise ValueError("Модель сгенерировала некорректный или пустой код.")
337
-
338
- # Очистка от возможных артефактов
339
- if generated_text.strip().startswith("```python"):
340
- generated_text = generated_text.strip()[9:]
341
- if generated_text.strip().endswith("```"):
342
- generated_text = generated_text.strip()[:-3]
343
-
 
 
 
 
 
 
 
 
 
344
  return generated_text.strip()
345
 
346
  except Exception as e:
347
- logging.error(f"Error generating content with GenAI: {e}")
348
- if "API key not valid" in str(e):
349
- raise ValueError("Внутренняя ошибка конфигурации API.")
350
- raise ValueError(f"Ошибка при генерации Python-кода сайта: {e}")
351
 
352
- def find_free_port():
353
- """Находит свободный TCP порт."""
354
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
355
- s.bind(('', 0))
356
- return s.getsockname()[1]
357
-
358
- def start_app_process(site_uuid, filepath, prompt):
359
- """��апускает сгенерированное приложение в отдельном процессе."""
360
- port = find_free_port()
361
- env = os.environ.copy()
362
- env["PORT"] = str(port)
363
 
364
- try:
365
- process = subprocess.Popen(
366
- ['python', filepath],
367
- env=env,
368
- stdout=open(f'{GENERATED_APPS_DIR}/{site_uuid}.log', 'w'),
369
- stderr=subprocess.STDOUT
370
- )
371
- time.sleep(2) # Даем время на запуск
372
 
373
- if process.poll() is not None: # Проверяем, не завершился ли процесс с ошибкой
374
- raise RuntimeError(f"Процесс для сайта {site_uuid} не запустился. Смотрите лог {site_uuid}.log")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
 
376
- RUNNING_APPS[site_uuid] = {
377
- "process": process,
378
- "port": port,
379
- "prompt": prompt,
380
- "filepath": filepath,
381
- }
382
- logging.info(f"Successfully started app {site_uuid} on port {port} with PID {process.pid}")
383
- return True
384
- except Exception as e:
385
- logging.error(f"Failed to start app {site_uuid}: {e}", exc_info=True)
386
- return False
387
 
388
- # --- Маршруты Flask ---
389
 
 
390
  @app.route('/')
391
  def index():
392
- return Response(html_template, mimetype='text/html')
393
-
394
- @app.route('/app/<site_uuid>/', defaults={'path': ''})
395
- @app.route('/app/<site_uuid>/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
396
- def proxy_to_generated_app(site_uuid, path):
397
- """Проксирует запросы к сгенерированным приложениям."""
398
- if site_uuid not in RUNNING_APPS:
399
- return "Приложение не найдено или не запущено.", 404
400
-
401
- port = RUNNING_APPS[site_uuid]['port']
402
- target_url = f"http://127.0.0.1:{port}/{path}"
403
-
404
- try:
405
- resp = requests.request(
406
- method=request.method,
407
- url=target_url,
408
- headers={key: value for (key, value) in request.headers if key != 'Host'},
409
- data=request.get_data(),
410
- cookies=request.cookies,
411
- allow_redirects=False,
412
- stream=True,
413
- params=request.args
414
- )
415
-
416
- headers = [(name, value) for (name, value) in resp.raw.headers.items()]
417
-
418
- return Response(resp.iter_content(chunk_size=1024), resp.status_code, headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
- except requests.exceptions.RequestException as e:
421
- logging.error(f"Proxy error for app {site_uuid}: {e}")
422
- return f"Ошибка при подключении к приложению {site_uuid}.", 502
 
 
 
423
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  @app.route('/generate', methods=['POST'])
426
  def handle_generate():
427
  user_prompt = request.form.get('prompt')
428
- if not user_prompt:
429
- return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
430
 
431
  try:
432
- python_code = generate_app_code_from_prompt(user_prompt)
 
433
 
434
- site_uuid = str(uuid.uuid4())
435
- filename = f"{site_uuid}.py"
436
- filepath = os.path.join(GENERATED_APPS_DIR, filename)
 
437
 
438
  with open(filepath, "w", encoding="utf-8") as f:
439
- f.write(python_code)
 
 
 
440
 
441
- logging.info(f"Generated Python code saved to {filepath}")
 
 
 
442
 
443
- if not start_app_process(site_uuid, filepath, user_prompt):
444
- return jsonify({"error": f"Сгенерированный код не удалось запустить. Проверьте лог {site_uuid}.log"}), 500
445
-
446
- save_data(RUNNING_APPS) # Сохраняем новое состояние
447
-
448
- site_url = f"/app/{site_uuid}/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  return jsonify({"site_url": site_url})
450
 
451
- except ValueError as ve:
452
- return jsonify({"error": str(ve)}), 400
 
453
  except Exception as e:
454
- logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
455
- return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
456
-
457
- # --- Функции запуска и очистки ---
458
-
459
- def cleanup_processes():
460
- """Останавливает все дочерние процессы при выходе."""
461
- logging.info("Shutting down... Terminating all running app processes.")
462
- for site_uuid, details in RUNNING_APPS.items():
463
- if details.get("process"):
464
- logging.info(f"Terminating process for site {site_uuid} (PID: {details['process'].pid})")
465
- details["process"].terminate()
466
- details["process"].wait() # Ждем завершения
467
- logging.info("All child processes terminated.")
468
-
469
- def restart_running_apps(saved_apps):
470
- """Перезапускает приложения, которые были активны до перезагрузки."""
471
- logging.info("Restarting previously running applications...")
472
- for site_uuid, details in saved_apps.items():
473
- filepath = details.get("filepath")
474
- prompt = details.get("prompt")
475
- if filepath and os.path.exists(filepath):
476
- logging.info(f"Attempting to restart app {site_uuid} from {filepath}")
477
- start_app_process(site_uuid, filepath, prompt)
478
- else:
479
- logging.warning(f"Could not restart app {site_uuid}, file {filepath} not found.")
480
-
481
- if saved_apps:
482
- save_data(RUNNING_APPS) # Сохраняем обновленные PIDы и порты
483
 
 
484
  if __name__ == '__main__':
485
- atexit.register(cleanup_processes)
 
486
 
487
  logging.info("Application starting up...")
488
 
489
- # 1. Скачиваем последнее состояние с HF
490
- if download_db_from_hf(DATA_FILE):
491
- saved_state = load_data()
492
- if saved_state:
493
- # 2. Перезапускаем ранее активные приложения
494
- restart_running_apps(saved_state)
495
-
496
- # 3. Запускаем фоновый бэкап, если есть токен
497
  if HF_TOKEN_WRITE:
498
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
499
  backup_thread.start()
500
- logging.info("Periodic backup thread started.")
501
  else:
502
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
503
 
504
- # 4. Запускаем основное приложение
505
- port = int(os.environ.get('PORT', 7860))
506
- logging.info(f"Starting main Flask app on host 0.0.0.0 and port {port}")
507
- app.run(host='0.0.0.0', port=port, debug=False)
 
4
  import logging
5
  import threading
6
  import time
 
7
  import subprocess
8
+ import sys
9
  import atexit
10
  from datetime import datetime
11
+ from flask import Flask, request, jsonify, Response, redirect, url_for, flash
12
+ from werkzeug.utils import secure_filename
13
+ from dotenv import load_dotenv
14
  import google.generativeai as genai
15
  from huggingface_hub import HfApi, hf_hub_download
16
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
17
  import requests
 
 
 
18
 
19
+ # --- Load Environment Variables ---
20
  load_dotenv()
21
+
22
+ # --- Configuration ---
23
  app = Flask(__name__)
24
+ app.secret_key = 'super-secret-key-for-generator-app-12345'
25
 
26
+ # Google AI Configuration
27
+ API_KEY_INTERNAL = os.getenv("GEMINI_API_KEY", "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns")
 
28
 
29
+ # Hugging Face Configuration
30
+ REPO_ID = "Kgshop/testsynk"
 
 
31
  HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
32
  HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
 
 
33
 
34
+ # Local Directories and Files
35
+ GENERATED_SITES_DIR = 'generated_sites'
36
+ DATA_FILE = 'generator_data.json' # State file for the generator app
37
+ SYNC_FILES = [DATA_FILE]
38
 
39
+ # Runtime State
40
+ running_sites = {} # { 'site_id': {'process': Popen_object, 'port': int, 'prompt': str} }
41
+ sites_data = {'sites': {}} # This will be loaded from DATA_FILE
42
+ next_available_port = 7861 # Main app runs on 7860
43
 
44
+ # Logging Configuration
45
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ # --- Hugging Face Backup & Restore Functions (Adapted from Code 2) ---
48
 
49
+ def download_db_from_hf(specific_file=None, retries=3, delay=5):
50
+ if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
 
51
  logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
52
+ token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
53
  files_to_download = [specific_file] if specific_file else SYNC_FILES
54
  logging.info(f"Attempting download for {files_to_download} from {REPO_ID}...")
55
+ all_successful = True
56
+
57
  for file_name in files_to_download:
58
+ success = False
59
  for attempt in range(retries + 1):
60
  try:
61
+ logging.info(f"Downloading {file_name} (Attempt {attempt + 1}/{retries + 1})...")
62
  hf_hub_download(
63
  repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use,
64
  local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False
65
  )
66
  logging.info(f"Successfully downloaded {file_name}.")
67
+ success = True
68
+ break
69
+ except RepositoryNotFoundError:
70
+ logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
71
+ return False
72
  except HfHubHTTPError as e:
73
  if e.response.status_code == 404:
74
+ logging.warning(f"File {file_name} not found in repo {REPO_ID}. Creating empty local file.")
75
+ if file_name == DATA_FILE:
76
+ with open(file_name, 'w', encoding='utf-8') as f:
77
+ json.dump({'sites': {}}, f)
78
+ success = True # We consider this a success as we handled it.
79
+ break
80
+ else:
81
+ logging.error(f"HTTP error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
82
  except Exception as e:
83
+ logging.error(f"Unexpected error downloading {file_name} (Attempt {attempt + 1}): {e}. Retrying...", exc_info=True)
84
+ if attempt < retries:
85
+ time.sleep(delay)
86
+ if not success:
87
+ logging.error(f"Failed to download {file_name} after {retries + 1} attempts.")
88
+ all_successful = False
89
+ return all_successful
90
 
91
  def upload_db_to_hf(specific_file=None):
92
  if not HF_TOKEN_WRITE:
93
+ logging.warning("HF_TOKEN (for writing) not set. Skipping upload to Hugging Face.")
94
  return
95
  try:
96
  api = HfApi()
97
  files_to_upload = [specific_file] if specific_file else SYNC_FILES
98
+ logging.info(f"Starting upload of {files_to_upload} to HF repo {REPO_ID}...")
99
  for file_name in files_to_upload:
100
  if os.path.exists(file_name):
101
  api.upload_file(
102
  path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID,
103
  repo_type="dataset", token=HF_TOKEN_WRITE,
104
+ commit_message=f"Sync generator state {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
105
  )
106
  logging.info(f"File {file_name} successfully uploaded to Hugging Face.")
107
+ else:
108
+ logging.warning(f"File {file_name} not found locally, skipping upload.")
109
  except Exception as e:
110
+ logging.error(f"Error during Hugging Face upload: {e}", exc_info=True)
111
 
112
  def periodic_backup():
113
  while True:
114
+ time.sleep(1800) # 30 minutes
115
  logging.info("Starting periodic backup...")
116
+ upload_db_to_hf()
 
 
 
117
 
118
  def load_data():
119
+ global sites_data
120
+ default_data = {'sites': {}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  try:
122
+ if not os.path.exists(DATA_FILE):
123
+ raise FileNotFoundError
124
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
125
+ data = json.load(file)
126
+ if 'sites' not in data: data['sites'] = {}
127
+ sites_data = data
128
+ logging.info(f"Local state loaded successfully from {DATA_FILE}")
129
+ except (FileNotFoundError, json.JSONDecodeError):
130
+ logging.warning(f"Local file {DATA_FILE} not found or corrupt. Attempting download from HF.")
131
+ if download_db_from_hf(specific_file=DATA_FILE):
132
+ try:
133
+ with open(DATA_FILE, 'r', encoding='utf-8') as file:
134
+ sites_data = json.load(file)
135
+ if 'sites' not in sites_data: sites_data['sites'] = {}
136
+ logging.info(f"State loaded from {DATA_FILE} after download.")
137
+ except (FileNotFoundError, json.JSONDecodeError):
138
+ logging.error(f"Failed to read file after download. Using default state.")
139
+ sites_data = default_data
140
+ else:
141
+ logging.error(f"Failed to download state from HF. Using empty default state.")
142
+ sites_data = default_data
143
 
144
+ def save_data():
145
+ try:
146
+ with open(DATA_FILE, 'w', encoding='utf-8') as file:
147
+ json.dump(sites_data, file, ensure_ascii=False, indent=4)
148
+ logging.info(f"Generator state saved to {DATA_FILE}")
149
+ # Upload to HF after every save for real-time backup
150
+ upload_db_to_hf(specific_file=DATA_FILE)
151
+ except Exception as e:
152
+ logging.error(f"Error saving generator state: {e}", exc_info=True)
153
 
154
+ # --- Core AI Generation Logic (Modified for Flask Apps) ---
155
 
156
+ def generate_flask_app_code_from_prompt(user_prompt):
 
 
 
157
  try:
158
  genai.configure(api_key=API_KEY_INTERNAL)
159
  except Exception as e:
160
+ raise ValueError(f"Failed to configure Google AI: {e}")
 
161
 
162
  if not user_prompt or not user_prompt.strip():
163
+ raise ValueError("Prompt cannot be empty.")
164
 
 
165
  system_instruction = (
166
+ "You are an expert Python Flask web developer. Your task is to generate a complete, single-file Flask application "
167
+ "based on the user's request. The entire application must be in ONE SINGLE Python (.py) file.\n"
168
+ "Crucial requirements for the generated file:\n"
169
+ "1. **Single File:** Everything must be in one Python file. No external templates, CSS, or JS files.\n"
170
+ "2. **HTML as Strings:** All HTML/CSS/JS for templates must be stored in multiline Python string variables (e.g., `HOME_PAGE_TEMPLATE = '''<!DOCTYPE html>...'''`).\n"
171
+ "3. **Render with `render_template_string`:** Use `from flask import render_template_string` to render the HTML string variables.\n"
172
+ "4. **Command-Line Port:** The application MUST accept the port number as a command-line argument. This is NOT optional. The main script will launch it like `python generated_app.py 8001`. "
173
+ " Use this exact structure at the end of the file:\n"
174
+ " ```python\n"
175
+ " import sys\n"
176
+ " if __name__ == '__main__':\n"
177
+ " # Default port is 5000, but it will be overridden by the command line argument.\n"
178
+ " port = int(sys.argv[1]) if len(sys.argv) > 1 else 5000\n"
179
+ " app.run(host='0.0.0.0', port=port, debug=False)\n"
180
+ " ```\n"
181
+ "5. **Database:** For any data storage, use a simple in-memory Python list or dictionary defined at the top of the script. Do not use file-based databases like SQLite or JSON files.\n"
182
+ "6. **Dependencies:** Only use Flask and standard Python libraries. Do not import libraries that are not commonly available, like pandas or numpy, unless absolutely necessary for the request.\n"
183
+ "7. **Output Format:** Directly output ONLY the Python code. Do not include any explanatory text, markdown formatting (like ```python), or anything else before `import os` or after the `app.run(...)` call."
184
  )
185
 
186
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
187
 
188
  try:
189
+ model = genai.GenerativeModel('gemini-1.5-flash-latest') # Using a more recent model
190
  response = model.generate_content(full_prompt)
191
 
192
+ generated_text = response.text.strip()
193
+
194
+ if not generated_text:
195
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason:
196
+ reason = response.prompt_feedback.block_reason
197
+ raise ValueError(f"Content generation blocked due to safety policy (reason: {reason}). Please try a different prompt.")
198
+ else:
199
+ raise ValueError("The model returned an empty result. Please try rephrasing your prompt.")
200
+
201
+ # Clean up potential markdown formatting
202
+ if generated_text.startswith("```python"):
203
+ generated_text = generated_text[10:]
204
+ if generated_text.endswith("```"):
205
+ generated_text = generated_text[:-3]
206
+
207
+ # Basic validation
208
+ if "from flask import Flask" not in generated_text or "app.run" not in generated_text:
209
+ raise ValueError("The generated code does not appear to be a valid Flask application.")
210
+
211
  return generated_text.strip()
212
 
213
  except Exception as e:
214
+ logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
215
+ raise ValueError(f"An error occurred while generating the site code: {e}")
 
 
216
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ # --- Process and Port Management ---
 
 
 
 
 
 
 
219
 
220
+ def find_free_port():
221
+ global next_available_port
222
+ # In a real app, you'd check if the port is actually free.
223
+ # For this demo, we'll just increment.
224
+ port = next_available_port
225
+ next_available_port += 1
226
+ return port
227
+
228
+ def cleanup():
229
+ logging.info("Shutting down generator... terminating all running site processes.")
230
+ for site_id, site_info in list(running_sites.items()):
231
+ try:
232
+ logging.info(f"Terminating site {site_id} (PID: {site_info['process'].pid})")
233
+ site_info['process'].terminate()
234
+ site_info['process'].wait(timeout=5) # Wait for graceful termination
235
+ except subprocess.TimeoutExpired:
236
+ logging.warning(f"Process for site {site_id} did not terminate gracefully, killing.")
237
+ site_info['process'].kill()
238
+ except Exception as e:
239
+ logging.error(f"Error terminating process for site {site_id}: {e}")
240
+ logging.info("Cleanup complete.")
241
 
242
+ atexit.register(cleanup)
 
 
 
 
 
 
 
 
 
 
243
 
244
+ # --- Flask Routes ---
245
 
246
+ # The main UI from Code 1, slightly adapted
247
  @app.route('/')
248
  def index():
249
+ # This is the HTML for the generator's frontend
250
+ html_template = """
251
+ <!DOCTYPE html>
252
+ <html lang="ru">
253
+ <head>
254
+ <meta charset="UTF-8">
255
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
256
+ <title>EVA - Генератор Python/Flask Сайтов</title>
257
+ <style>
258
+ :root {
259
+ --system-gray-100-light: #f2f2f7; --system-gray-75-light: #f8f8fa; --system-gray-50-light: #ffffff; --system-gray-dark-100-light: #000000; --system-gray-dark-75-light: #1c1c1e; --system-gray-dark-50-light: #3a3a3c; --system-gray-light-75-light: #8e8e93; --system-gray-light-50-light: #aeaeb2; --system-blue-light: #007aff; --system-blue-light-hover: #005ecf; --system-red-light: #ff3b30; --system-separator-light: rgba(60, 60, 67, 0.29); --system-separator-opaque-light: #d1d1d6;
260
+ --system-gray-100-dark: #1c1c1e; --system-gray-75-dark: #2c2c2e; --system-gray-50-dark: #000000; --system-gray-dark-100-dark: #ffffff; --system-gray-dark-75-dark: #f2f2f7; --system-gray-dark-50-dark: #e5e5ea; --system-gray-light-75-dark: #8e8e93; --system-gray-light-50-dark: #636366; --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff; --system-red-dark: #ff453a; --system-separator-dark: rgba(84, 84, 88, 0.65); --system-separator-opaque-dark: #38383a;
261
+ }
262
+ @media (prefers-color-scheme: dark) { :root { --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark); --text-color: var(--system-gray-dark-100-dark); --secondary-text-color: var(--system-gray-light-75-dark); --tertiary-text-color: var(--system-gray-light-50-dark); --border-color: var(--system-separator-dark); --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-75-dark); --primary-color: var(--system-blue-dark); --primary-color-hover: var(--system-blue-dark-hover); --error-color: var(--system-red-dark); } }
263
+ @media (prefers-color-scheme: light) { :root { --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light); --text-color: var(--system-gray-dark-100-light); --secondary-text-color: var(--system-gray-light-75-light); --tertiary-text-color: var(--system-gray-light-50-light); --border-color: var(--system-separator-light); --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-75-light); --primary-color: var(--system-blue-light); --primary-color-hover: var(--system-blue-light-hover); --error-color: var(--system-red-light); } }
264
+ html { height: -webkit-fill-available; }
265
+ body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom); background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; min-height: -webkit-fill-available; line-height: 1.45; }
266
+ .container { background-color: var(--content-bg); padding: 25px 30px 30px 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05); max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
267
+ h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
268
+ p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
269
+ .form-group { margin-bottom: 28px; }
270
+ label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
271
+ textarea#prompt-input { width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px; font-size: 16px; background-color: var(--input-bg); color: var(--text-color); box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease; font-family: inherit; resize: vertical; min-height: 120px; }
272
+ textarea#prompt-input:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent); outline: none; }
273
+ button#generate-button { width: 100%; padding: 16px; background-color: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer; transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px; }
274
+ button#generate-button:hover { background-color: var(--primary-color-hover); }
275
+ button#generate-button:active { transform: scale(0.98); }
276
+ button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
277
+ .output-section { margin-top: 35px; } .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
278
+ button#copy-button { background-color: transparent; border: none; color: var(--primary-color); font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px; border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none; } button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); } button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); } button#copy-button.copied { color: #34c759; }
279
+ #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word; font-size: 15px; color: var(--text-color); line-height: 1.5; transition: border-color 0.2s ease, background-color 0.2s ease; display: flex; align-items: center; justify-content: center; } #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; } #output-container a:hover { text-decoration: underline; }
280
+ #output-container.loading::before { content: "Генерация и запуск сайта..."; display: block; text-align: center; font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out; }
281
+ #output-container.error { color: var(--error-color); font-weight: 500; border-color: color-mix(in srgb, var(--error-color) 50%, transparent); background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg)); justify-content: flex-start; }
282
+ @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <div class="container">
287
+ <h1>EVA</h1>
288
+ <p class="subtitle">Генератор Python/Flask сайтов</p>
289
+ <form id="generate-form">
290
+ <div class="form-group">
291
+ <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать</label>
292
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай сайт-визитку для фотографа. На главной странице должны быть галерея работ (3-4 фото-заглушки), раздел 'Обо мне' и контактная форма."></textarea>
293
+ </div>
294
+ <button type="submit" id="generate-button">Создать и запустить сайт</button>
295
+ </form>
296
+ <div class="output-section">
297
+ <div class="output-header">
298
+ <label id="output-label">Ссылка на запущенный сайт</label>
299
+ <button id="copy-button">Копировать</button>
300
+ </div>
301
+ <div id="output-container" aria-live="polite"></div>
302
+ </div>
303
+ </div>
304
+ <script>
305
+ const form = document.getElementById('generate-form');
306
+ const outputContainer = document.getElementById('output-container');
307
+ const generateButton = document.getElementById('generate-button');
308
+ const copyButton = document.getElementById('copy-button');
309
+
310
+ form.addEventListener('submit', async (event) => {
311
+ event.preventDefault();
312
+ const formData = new FormData(form);
313
+ if (!formData.get('prompt').trim()) {
314
+ showError("Пожалуйста, опишите сайт."); return;
315
+ }
316
+ generateButton.disabled = true; generateButton.textContent = 'Генерация...';
317
+ outputContainer.innerHTML = ''; outputContainer.classList.add('loading');
318
+ outputContainer.classList.remove('error'); copyButton.style.display = 'none';
319
+
320
+ try {
321
+ const response = await fetch('/generate', { method: 'POST', body: formData });
322
+ const result = await response.json();
323
+ if (!response.ok) { throw new Error(result.error || `Ошибка сервера: ${response.status}`); }
324
+
325
+ if (result.site_url) {
326
+ const link = document.createElement('a');
327
+ link.href = result.site_url;
328
+ link.textContent = result.site_url;
329
+ link.target = "_blank";
330
+ outputContainer.innerHTML = '';
331
+ outputContainer.appendChild(link);
332
+ copyButton.style.display = 'block';
333
+ copyButton.dataset.copyText = result.site_url;
334
+ } else {
335
+ showError(result.error || "Не удалось получить URL сайта.");
336
+ }
337
+ } catch (error) {
338
+ console.error("Fetch Error:", error);
339
+ showError(`Ошибка: ${error.message}`);
340
+ } finally {
341
+ generateButton.disabled = false; generateButton.textContent = 'Создать и запустить сайт';
342
+ outputContainer.classList.remove('loading');
343
+ }
344
+ });
345
 
346
+ copyButton.addEventListener('click', () => {
347
+ navigator.clipboard.writeText(copyButton.dataset.copyText).then(() => {
348
+ copyButton.textContent = 'Скопировано!';
349
+ setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
350
+ });
351
+ });
352
 
353
+ function showError(message) {
354
+ outputContainer.innerHTML = `<span>${message}</span>`;
355
+ outputContainer.classList.add('error');
356
+ outputContainer.classList.remove('loading');
357
+ copyButton.style.display = 'none';
358
+ }
359
+ </script>
360
+ </body>
361
+ </html>
362
+ """
363
+ return Response(html_template, mimetype='text/html')
364
 
365
  @app.route('/generate', methods=['POST'])
366
  def handle_generate():
367
  user_prompt = request.form.get('prompt')
368
+ if not user_prompt or not user_prompt.strip():
369
+ return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
370
 
371
  try:
372
+ # 1. Generate the Python code for the Flask app
373
+ flask_app_code = generate_flask_app_code_from_prompt(user_prompt)
374
 
375
+ # 2. Save the generated code to a .py file
376
+ site_id = str(uuid.uuid4())
377
+ filename = f"{site_id}.py"
378
+ filepath = os.path.join(GENERATED_SITES_DIR, filename)
379
 
380
  with open(filepath, "w", encoding="utf-8") as f:
381
+ f.write(flask_app_code)
382
+
383
+ # 3. Find a free port and launch the new Flask app in a separate process
384
+ port = find_free_port()
385
 
386
+ # Pass the HF token to the child process environment if needed
387
+ env = os.environ.copy()
388
+ if HF_TOKEN_WRITE:
389
+ env["HF_TOKEN"] = HF_TOKEN_WRITE
390
 
391
+ process = subprocess.Popen(
392
+ [sys.executable, filepath, str(port)],
393
+ stdout=subprocess.PIPE,
394
+ stderr=subprocess.PIPE,
395
+ env=env
396
+ )
397
+
398
+ # Give it a moment to start up or fail
399
+ time.sleep(3)
400
+
401
+ # Check if the process has already exited with an error
402
+ if process.poll() is not None:
403
+ stderr_output = process.stderr.read().decode('utf-8', errors='ignore')
404
+ raise RuntimeError(f"Generated app failed to start. Error:\n{stderr_output}")
405
+
406
+ # 4. Store the running process and update state
407
+ running_sites[site_id] = {
408
+ 'process': process,
409
+ 'port': port,
410
+ 'prompt': user_prompt,
411
+ 'id': site_id
412
+ }
413
+
414
+ # 5. Persist the state
415
+ sites_data['sites'][site_id] = {'port': port, 'prompt': user_prompt, 'id': site_id}
416
+ save_data()
417
+
418
+ # 6. Return the URL to the user
419
+ host = request.host.split(':')[0]
420
+ site_url = f"http://{host}:{port}"
421
+
422
+ logging.info(f"Successfully generated and launched site {site_id} on port {port}")
423
  return jsonify({"site_url": site_url})
424
 
425
+ except (ValueError, RuntimeError) as e:
426
+ logging.error(f"Error during generation or launch: {e}", exc_info=True)
427
+ return jsonify({"error": str(e)}), 400
428
  except Exception as e:
429
+ logging.error(f"Unexpected server error: {e}", exc_info=True)
430
+ return jsonify({"error": f"An unexpected internal error occurred: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
+ # --- Main Application Start ---
433
  if __name__ == '__main__':
434
+ if not os.path.exists(GENERATED_SITES_DIR):
435
+ os.makedirs(GENERATED_SITES_DIR)
436
 
437
  logging.info("Application starting up...")
438
 
439
+ # Attempt to load state from HF on startup
440
+ load_data()
441
+ # Note: We are not auto-relaunching sites on startup to keep it simple,
442
+ # but the state is preserved in generator_data.json. An admin panel could show them.
443
+
 
 
 
444
  if HF_TOKEN_WRITE:
445
  backup_thread = threading.Thread(target=periodic_backup, daemon=True)
446
  backup_thread.start()
447
+ logging.info("Periodic state backup thread started.")
448
  else:
449
  logging.warning("Periodic backup will NOT run (HF_TOKEN for writing not set).")
450
 
451
+ logging.info("Starting main generator Flask app on host 0.0.0.0 and port 7860")
452
+ app.run(host='0.0.0.0', port=7860, debug=False)