Kgshop commited on
Commit
0b8c38a
·
verified ·
1 Parent(s): 9917d4a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +576 -527
app.py CHANGED
@@ -1,531 +1,558 @@
1
  import os
2
  import uuid
3
- import subprocess
4
- import time
5
- import socket
6
  import logging
7
- import atexit
 
8
  from datetime import datetime
9
- from flask import Flask, request, jsonify, Response, send_from_directory, redirect, url_for
10
- import google.generativeai as genai
11
- from huggingface_hub import HfApi, hf_hub_download
12
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
13
- import requests # Нужен для HfHubHTTPError
 
 
 
 
 
 
14
 
15
- # --- Конфигурация ---
16
  app = Flask(__name__)
 
17
 
18
- # ВНИМАНИЕ: Жестко закодированный API ключ - это небезопасно. Используйте переменные окружения.
19
- API_KEY_INTERNAL = "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns" # Используется ключ из Кода 1
20
- SITES_CACHE_DIR = 'generated_sites_cache' # Локальный кэш для .py файлов сайтов
21
 
22
- # Конфигурация Hugging Face (адаптировано из Кода 2)
23
- REPO_ID = "Kgshop/testsynk" # Указанный репозиторий
24
- HF_TOKEN_WRITE = os.getenv("HF_TOKEN") # Токен с правом записи
25
- HF_TOKEN_READ = os.getenv("HF_TOKEN_READ", HF_TOKEN_WRITE) # Токен для чтения (или тот же, что и для записи)
 
 
 
26
 
27
  DOWNLOAD_RETRIES = 3
28
  DOWNLOAD_DELAY = 5
 
29
 
30
- # Управление запущенными сайтами
31
- RUNNING_SITES = {} # {'site_filename.py': {'process': Popen_object, 'port': 5001, 'host': '127.0.0.1'}}
32
- BASE_PORT_FOR_GENERATED_SITES = 8001
33
- USED_PORTS = set()
34
-
35
- # --- Настройка Логирования ---
36
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
37
 
38
- # --- Создание директорий ---
39
- if not os.path.exists(SITES_CACHE_DIR):
40
- os.makedirs(SITES_CACHE_DIR)
41
 
42
- # --- Функции для работы с Hugging Face ---
43
- def download_file_from_hf(repo_filename, local_save_path, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
44
- if not HF_TOKEN_READ:
45
- logging.warning("HF_TOKEN_READ not set. Download might fail for private repos.")
46
-
47
- token_to_use = HF_TOKEN_READ
48
 
49
- logging.info(f"Attempting download for {repo_filename} from {REPO_ID} to {local_save_path}...")
50
- for attempt in range(retries + 1):
51
- try:
52
- logging.info(f"Downloading {repo_filename} (Attempt {attempt + 1}/{retries + 1})...")
53
- downloaded_path = hf_hub_download(
54
- repo_id=REPO_ID,
55
- filename=repo_filename,
56
- repo_type="dataset", # Обычно 'dataset' для файлов, или 'model'
57
- token=token_to_use,
58
- local_dir=os.path.dirname(local_save_path),
59
- local_dir_use_symlinks=False,
60
- force_download=True, # Всегда скачивать свежую версию
61
- resume_download=False,
62
- # Важно указать имя файла, если local_dir указан, иначе он создаст структуру папок репозитория
63
- # hf_hub_download вернет полный путь к файлу, который может включать структуру папок репозитория.
64
- # Мы должны убедиться, что он сохраняется как local_save_path
65
- )
66
- # Переименовываем/перемещаем, если hf_hub_download скачал не точно по пути
67
- if downloaded_path != local_save_path:
68
- os.makedirs(os.path.dirname(local_save_path), exist_ok=True)
69
- os.replace(downloaded_path, local_save_path) # Используем replace для атомарности
70
- logging.info(f"File moved from {downloaded_path} to {local_save_path}")
71
 
72
- logging.info(f"Successfully downloaded {repo_filename} to {local_save_path}.")
73
- return True
74
- except RepositoryNotFoundError:
75
- logging.error(f"Repository {REPO_ID} not found. Download cancelled.")
76
- return False
77
- except HfHubHTTPError as e:
78
- if e.response.status_code == 404:
79
- logging.warning(f"File {repo_filename} not found in repo {REPO_ID} (404).")
80
- return False # Файл не найден, нет смысла ретраить
81
- logging.error(f"HTTP error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
82
- except requests.exceptions.RequestException as e: # requests используется внутри huggingface_hub
83
- logging.error(f"Network error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...")
84
- except Exception as e:
85
- logging.error(f"Unexpected error downloading {repo_filename} (Attempt {attempt + 1}): {e}. Retrying in {delay}s...", exc_info=True)
86
-
87
- if attempt < retries:
88
- time.sleep(delay)
89
-
90
- logging.error(f"Failed to download {repo_filename} after {retries + 1} attempts.")
91
- return False
92
 
93
- def upload_file_to_hf(local_filepath, path_in_repo):
94
  if not HF_TOKEN_WRITE:
95
- logging.warning("HF_TOKEN_WRITE (for writing) not set. Skipping upload to Hugging Face.")
96
  return False
97
- if not os.path.exists(local_filepath):
98
- logging.error(f"Local file {local_filepath} not found. Skipping upload.")
99
  return False
 
 
 
 
100
  try:
101
- api = HfApi()
102
- logging.info(f"Starting upload of {local_filepath} to {path_in_repo} in HF repo {REPO_ID}...")
103
  api.upload_file(
104
- path_or_fileobj=local_filepath,
105
  path_in_repo=path_in_repo,
106
  repo_id=REPO_ID,
107
  repo_type="dataset",
108
  token=HF_TOKEN_WRITE,
109
- commit_message=f"Sync {path_in_repo} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
110
  )
111
- logging.info(f"File {local_filepath} successfully uploaded to {path_in_repo} in Hugging Face.")
112
  return True
113
  except Exception as e:
114
- logging.error(f"Error uploading file {local_filepath} to Hugging Face: {e}", exc_info=True)
115
  return False
116
 
117
- # --- Функции управления дочерними сайтами ---
118
- def find_free_port(start_port):
119
- port = start_port
120
- while True:
121
- if port in USED_PORTS:
122
- port += 1
123
- continue
124
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
125
- try:
126
- s.bind(('127.0.0.1', port))
127
- USED_PORTS.add(port)
128
- return port
129
- except OSError:
130
- logging.warning(f"Port {port} is already in use by another application, trying next.")
131
- # Добавляем в USED_PORTS, чтобы не пробовать его снова в этой сессии для наших сайтов
132
- USED_PORTS.add(port)
133
- port += 1
134
- if port > 65535:
135
- raise RuntimeError("Could not find a free port.")
136
-
137
-
138
- def start_generated_site(py_filename):
139
- if py_filename in RUNNING_SITES:
140
- site_info = RUNNING_SITES[py_filename]
141
- # Проверить, жив ли процесс
142
- if site_info['process'].poll() is None: # None значит, что процесс еще работает
143
- logging.info(f"Site {py_filename} is already running on port {site_info['port']}.")
144
- return site_info['host'], site_info['port']
145
- else:
146
- logging.warning(f"Site {py_filename} was marked as running but process died. Cleaning up.")
147
- USED_PORTS.discard(site_info['port'])
148
- del RUNNING_SITES[py_filename]
149
 
150
- local_py_path = os.path.join(SITES_CACHE_DIR, py_filename)
151
-
152
- # 1. Скачать/убедиться в наличии файла
153
- if not os.path.exists(local_py_path):
154
- if not download_file_from_hf(f"sites/{py_filename}", local_py_path):
155
- logging.error(f"Failed to download site {py_filename} from Hugging Face.")
156
- return None, None
157
-
158
- # 2. Найти свободный порт и модифицировать код сайта
159
- try:
160
- assigned_port = find_free_port(BASE_PORT_FOR_GENERATED_SITES)
161
- with open(local_py_path, 'r', encoding='utf-8') as f:
162
- original_code = f.read()
163
-
164
- # Заменить плейсхолдер порта
165
- modified_code = original_code.replace("{{GENERATED_APP_PORT}}", str(assigned_port))
166
-
167
- # Убедимся, что код пытается запуститься на 0.0.0.0 или 127.0.0.1
168
- # Это очень грубая проверка, лучше если AI генерирует предсказуемый app.run
169
- if f"app.run(host='0.0.0.0', port={assigned_port})" not in modified_code and \
170
- f"app.run(host='127.0.0.1', port={assigned_port})" not in modified_code and \
171
- f"app.run(port={assigned_port}, host='0.0.0.0')" not in modified_code and \
172
- f"app.run(port={assigned_port}, host='127.0.0.1')" not in modified_code:
173
-
174
- # Попробуем заменить более общий app.run(port=XXXX) или app.run()
175
- import re
176
- modified_code = re.sub(
177
- r"app\.run\((.*)\)",
178
- f"app.run(host='0.0.0.0', port={assigned_port})",
179
- modified_code,
180
- flags=re.DOTALL
181
- )
182
- if "app.run(" not in modified_code : # Если app.run не было вообще
183
- modified_code += f"\n\nif __name__ == '__main__':\n app.run(host='0.0.0.0', port={assigned_port})\n"
184
 
 
 
 
 
 
185
 
186
- with open(local_py_path, 'w', encoding='utf-8') as f:
187
- f.write(modified_code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
- except Exception as e:
190
- logging.error(f"Error preparing site {py_filename} for launch: {e}", exc_info=True)
191
- if 'assigned_port' in locals() and assigned_port:
192
- USED_PORTS.discard(assigned_port)
193
- return None, None
194
 
195
- # 3. Запустить скрипт
196
- try:
197
- # Для Windows, если python не в PATH, может понадобиться sys.executable
198
- # python_executable = sys.executable
199
- python_executable = "python" # Предполагаем, что python в PATH
200
- process = subprocess.Popen(
201
- [python_executable, local_py_path],
202
- stdout=subprocess.PIPE, # Можно перенаправить в лог-файлы
203
- stderr=subprocess.PIPE
204
- )
205
- time.sleep(2) # Дать время Flask-приложению запуститься
 
 
 
 
 
206
 
207
- if process.poll() is not None: # Если процесс сразу завершился
208
- stdout, stderr = process.communicate()
209
- logging.error(f"Site {py_filename} failed to start. Return code: {process.returncode}")
210
- logging.error(f"Stdout: {stdout.decode(errors='ignore')}")
211
- logging.error(f"Stderr: {stderr.decode(errors='ignore')}")
212
- USED_PORTS.discard(assigned_port)
213
- return None, None
214
 
215
- site_host = '127.0.0.1' # Сгенерированные сайты слушают на 0.0.0.0, но доступны через 127.0.0.1
216
- RUNNING_SITES[py_filename] = {'process': process, 'port': assigned_port, 'host': site_host}
217
- logging.info(f"Site {py_filename} started successfully on http://{site_host}:{assigned_port}")
218
- return site_host, assigned_port
219
- except Exception as e:
220
- logging.error(f"Error launching site {py_filename}: {e}", exc_info=True)
221
- if 'assigned_port' in locals() and assigned_port:
222
- USED_PORTS.discard(assigned_port)
223
- return None, None
224
-
225
- def cleanup_running_sites():
226
- logging.info("Cleaning up running generated sites...")
227
- for filename, site_info in list(RUNNING_SITES.items()): # list() для копии, т.к. будем удалять
228
  try:
229
- if site_info['process'].poll() is None: # Если процесс еще жив
230
- logging.info(f"Terminating site {filename} (PID: {site_info['process'].pid}) on port {site_info['port']}")
231
- site_info['process'].terminate() # Сначала пробуем мягко
232
- try:
233
- site_info['process'].wait(timeout=5) # Ждем завершения
234
- except subprocess.TimeoutExpired:
235
- logging.warning(f"Site {filename} did not terminate gracefully, killing.")
236
- site_info['process'].kill() # Если не помогло - жестко
237
- USED_PORTS.discard(site_info['port'])
238
- del RUNNING_SITES[filename]
239
- except Exception as e:
240
- logging.error(f"Error terminating site {filename}: {e}", exc_info=True)
241
- logging.info("Cleanup complete.")
 
 
 
 
 
 
 
 
 
 
 
242
 
243
- atexit.register(cleanup_running_sites)
244
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- # --- Функция генерации кода сайта с помощью Google AI ---
247
- def generate_full_python_site_from_prompt(user_prompt):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  try:
249
- # ВНИМАНИЕ: Ключ API жестко закодирован. В продакшене используйте переменные окружения.
250
  genai.configure(api_key=API_KEY_INTERNAL)
251
  except Exception as e:
252
- logging.error(f"Error configuring GenAI: {e}", exc_info=True)
253
  raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
254
 
255
  if not user_prompt or not user_prompt.strip():
256
  raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
257
 
258
  system_instruction = (
259
- "You are an expert Python web developer. Your task is to generate a complete, single-file Flask application "
260
- "based on the user's request. The entire application must be contained in a single .py file.\n"
261
- "Follow these requirements strictly:\n"
262
- "1. **Flask Application:** The code must be a runnable Flask application.\n"
263
- "2. **Single File:** All code, including HTML templates, CSS, and JavaScript, must be in this single Python file.\n"
264
- "3. **HTML Templates:** Embed HTML as Python strings. Use `flask.render_template_string()`.\n"
265
- "4. **CSS/JS:** Inline CSS within `<style>` tags in the HTML strings. Inline JavaScript within `<script>` tags in the HTML strings. Do not use external files for CSS/JS.\n"
266
- "5. **Routes:** Define Flask routes as requested by the user.\n"
267
- "6. **`app.run`:** Crucially, the script MUST end with a block like this for запуска:\n"
268
- " ```python\n"
269
- " if __name__ == '__main__':\n"
270
- " # IMPORTANT: The port number MUST be the placeholder {{GENERATED_APP_PORT}}\n"
271
- " # The host should be '0.0.0.0' to be accessible.\n"
272
- " app.run(host='0.0.0.0', port={{GENERATED_APP_PORT}})\n"
273
- " ```\n"
274
- "7. **No External File Dependencies:** The script should not rely on any other local files (e.g., for images, data) unless explicitly creating and managing them (e.g. a simple data.json). Images should preferably be placeholder URLs (e.g., from placehold.co) or base64 encoded if small and necessary.\n"
275
- "8. **Self-Contained Database (Optional):** If the site requires data persistence, it should use a simple mechanism like storing data in Python dictionaries or lists within the script itself. For more complex needs, it could read/write to a single JSON file named `data.json` *in its current working directory if absolutely necessary*, and the script should handle its creation. Avoid complex databases or ORMs.\n"
276
- "9. **Direct Python Output:** Output ONLY the Python code. Do NOT include ```python, markdown, or any explanatory text before or after the code block. The output must start with `import flask` or similar Python code and end with the `app.run` block.\n"
277
- "10. **Error Handling & Imports:** Include necessary imports (like `from flask import Flask, render_template_string, request, ...`). Add basic error handling if appropriate for the requested site.\n"
278
- "11. **Character Encoding:** Ensure any HTML generated uses UTF-8.\n"
279
- "12. **Simplicity:** Favor simplicity and clarity in the generated code."
 
 
 
 
 
 
 
 
 
 
 
 
280
  )
281
-
282
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
283
 
284
- response_obj = None # Для отладки prompt_feedback
285
  try:
286
- # Рекомендуется использовать более новые модели для генерации кода, например, Gemini
287
- model = genai.GenerativeModel('gemini-1.5-flash-latest') # Заменено с learnlm-2.0-flash-experimental
288
  response_obj = model.generate_content(full_prompt)
289
 
290
  generated_text = ""
291
- # Обработка ответа согласно документации Gemini
292
  if hasattr(response_obj, 'text') and response_obj.text:
293
  generated_text = response_obj.text
294
  elif hasattr(response_obj, 'parts') and response_obj.parts:
295
  generated_text = "".join(part.text for part in response_obj.parts if hasattr(part, 'text'))
296
 
297
  if not generated_text.strip():
298
- # Проверка на блокировку контента
299
- if hasattr(response_obj, 'prompt_feedback') and \
300
- hasattr(response_obj.prompt_feedback, 'block_reason') and \
301
- response_obj.prompt_feedback.block_reason:
302
- reason = response_obj.prompt_feedback.block_reason
303
- raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другой запрос.")
304
- else:
305
- raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
306
 
307
- # Очистка от markdown, если модель его добавила
308
  clean_text = generated_text.strip()
309
  if clean_text.startswith("```python"):
310
- clean_text = clean_text[9:] # Удаляем ```python
311
- if clean_text.endswith("```"):
312
- clean_text = clean_text[:-3] # Удаляем ```
313
- generated_text = clean_text.strip()
314
- elif clean_text.startswith("```"): # Общий случай для ``` без указания языка
315
- clean_text = clean_text[3:]
316
  if clean_text.endswith("```"):
317
  clean_text = clean_text[:-3]
318
- generated_text = clean_text.strip()
 
 
 
319
 
320
- if not (generated_text.startswith("import ") or generated_text.startswith("from ")):
321
- logging.warning(f"Generated code might not be pure Python. Preview: {generated_text[:200]}")
322
- # Дополнительная проверка не помешает, но AI должен следовать инструкциям
 
 
 
 
 
 
323
 
324
  return generated_text
325
 
326
  except Exception as e:
327
- logging.error(f"Error generating content with GenAI: {e}", exc_info=True)
328
  error_message = str(e)
329
- # Более детальная обработка ошибок API
330
  if "API key not valid" in error_message:
331
- raise ValueError("Внутренняя ошибка конфигурации API Google AI. Проверьте ключ.")
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' активна.")
336
- elif "resource has been exhausted" in error_message.lower() or "quota" in error_message.lower():
337
- raise ValueError("Квота запросов к Google AI исчерпана. Попробуйте позже.")
338
- elif "content has been blocked" in error_message.lower() or \
339
- (response_obj and hasattr(response_obj, 'prompt_feedback') and \
340
- hasattr(response_obj.prompt_feedback, 'block_reason') and \
341
- response_obj.prompt_feedback.block_reason):
342
- reason = "неизвестна"
343
- if response_obj and hasattr(response_obj, 'prompt_feedback') and hasattr(response_obj.prompt_feedback, 'block_reason') and response_obj.prompt_feedback.block_reason:
344
- reason = response_obj.prompt_feedback.block_reason
345
- elif "safety settings" in error_message.lower():
346
- reason = "настройки безопасности"
347
- raise ValueError(f"Генерация контента заблокирована (причина: {reason}). Попробуйте другой запрос.")
348
- # Ошибка из raise ValueError выше
349
- elif "Генерация контента заблокирована" in error_message or "Модель вернула пустой результат" in error_message:
350
- raise # Перебрасываем уже обработанную ошибку
351
  else:
352
- raise ValueError(f"Неизвестная ошибка при генерации Python-кода сайта: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
 
354
 
355
- # --- HTML шаблон для главной страницы генератора ---
356
- # (Взят из Кода 1 и немного адаптирован)
357
- html_template = """
358
  <!DOCTYPE html>
359
  <html lang="ru">
360
  <head>
361
  <meta charset="UTF-8">
362
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
363
- <title>EVA - Генератор Python Сайтов</title>
364
  <style>
365
- :root {
366
- --system-gray-100-light: #f2f2f7;
367
- --system-gray-75-light: #f8f8fa;
368
- --system-gray-50-light: #ffffff;
369
- --system-gray-dark-100-light: #000000;
370
- --system-gray-dark-75-light: #1c1c1e;
371
- --system-gray-dark-50-light: #3a3a3c;
372
- --system-gray-light-75-light: #8e8e93;
373
- --system-gray-light-50-light: #aeaeb2;
374
- --system-blue-light: #007aff;
375
- --system-blue-light-hover: #005ecf;
376
- --system-red-light: #ff3b30;
377
- --system-separator-light: rgba(60, 60, 67, 0.29);
378
- --system-separator-opaque-light: #d1d1d6;
379
-
380
- --system-gray-100-dark: #1c1c1e;
381
- --system-gray-75-dark: #2c2c2e;
382
- --system-gray-50-dark: #000000;
383
- --system-gray-dark-100-dark: #ffffff;
384
- --system-gray-dark-75-dark: #f2f2f7;
385
- --system-gray-dark-50-dark: #e5e5ea;
386
- --system-gray-light-75-dark: #8e8e93;
387
- --system-gray-light-50-dark: #636366;
388
- --system-blue-dark: #0a84ff;
389
- --system-blue-dark-hover: #3b9eff;
390
- --system-red-dark: #ff453a;
391
- --system-separator-dark: rgba(84, 84, 88, 0.65);
392
- --system-separator-opaque-dark: #38383a;
393
  }
394
-
395
  @media (prefers-color-scheme: dark) {
396
  :root {
397
- --bg-color: var(--system-gray-50-dark);
398
- --content-bg: var(--system-gray-100-dark);
399
- --text-color: var(--system-gray-dark-100-dark);
400
- --secondary-text-color: var(--system-gray-light-75-dark);
401
- --tertiary-text-color: var(--system-gray-light-50-dark);
402
- --border-color: var(--system-separator-dark);
403
- --border-color-opaque: var(--system-separator-opaque-dark);
404
- --input-bg: var(--system-gray-75-dark);
405
- --primary-color: var(--system-blue-dark);
406
- --primary-color-hover: var(--system-blue-dark-hover);
407
  --error-color: var(--system-red-dark);
408
  }
409
  }
410
-
411
  @media (prefers-color-scheme: light) {
412
  :root {
413
- --bg-color: var(--system-gray-100-light);
414
- --content-bg: var(--system-gray-50-light);
415
- --text-color: var(--system-gray-dark-100-light);
416
- --secondary-text-color: var(--system-gray-light-75-light);
417
- --tertiary-text-color: var(--system-gray-light-50-light);
418
- --border-color: var(--system-separator-light);
419
- --border-color-opaque: var(--system-separator-opaque-light);
420
- --input-bg: var(--system-gray-75-light);
421
- --primary-color: var(--system-blue-light);
422
- --primary-color-hover: var(--system-blue-light-hover);
423
  --error-color: var(--system-red-light);
424
  }
425
  }
426
-
427
- html { height: -webkit-fill-available; }
428
- body {
429
- font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
430
- margin: 0; padding: 20px env(safe-area-inset-top) 20px env(safe-area-inset-bottom);
431
- background-color: var(--bg-color); color: var(--text-color);
432
- display: flex; justify-content: center; align-items: flex-start;
433
- min-height: 100vh; min-height: -webkit-fill-available;
434
- line-height: 1.45; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
435
- }
436
- .container {
437
- background-color: var(--content-bg); padding: 25px 30px 30px 30px;
438
- border-radius: 24px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0,0,0,0.05);
439
- max-width: 580px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px;
440
- }
441
- h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; color: var(--text-color); letter-spacing: -0.5px; }
442
- p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; font-weight: 400; }
443
- .form-group { margin-bottom: 28px; }
444
- label.input-label { display: block; font-weight: 500; margin-bottom: 10px; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
445
- textarea#prompt-input {
446
- width: 100%; padding: 14px 18px; border: 1px solid var(--border-color-opaque); border-radius: 12px;
447
- font-size: 16px; background-color: var(--input-bg); color: var(--text-color);
448
- box-sizing: border-box; transition: border-color 0.2s ease, box-shadow 0.2s ease;
449
- font-family: inherit; resize: vertical; min-height: 120px;
450
- }
451
- textarea#prompt-input:focus {
452
- border-color: var(--primary-color);
453
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
454
- outline: none;
455
- }
456
- button#generate-button {
457
- width: 100%; padding: 16px; background-color: var(--primary-color); color: white;
458
- border: none; border-radius: 12px; font-size: 17px; font-weight: 600; cursor: pointer;
459
- transition: background-color 0.2s ease, transform 0.1s ease; margin-top: 15px;
460
- }
461
- button#generate-button:hover { background-color: var(--primary-color-hover); }
462
- button#generate-button:active { transform: scale(0.98); }
463
- button#generate-button:disabled { background-color: var(--tertiary-text-color); cursor: not-allowed; }
464
  .output-section { margin-top: 35px; }
465
- .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
466
- label#output-label { font-weight: 500; font-size: 15px; color: var(--secondary-text-color); padding-left: 5px; }
467
- button#copy-button {
468
- background-color: transparent; border: none; color: var(--primary-color);
469
- font-size: 14px; font-weight: 500; cursor: pointer; padding: 5px 8px;
470
- border-radius: 6px; transition: background-color 0.2s ease, color 0.2s ease; display: none;
471
- }
472
- button#copy-button:hover { background-color: color-mix(in srgb, var(--primary-color) 15%, transparent); }
473
- button#copy-button:active { background-color: color-mix(in srgb, var(--primary-color) 25%, transparent); }
474
- button#copy-button.copied { color: #34c759; }
475
- @media (prefers-color-scheme: dark) { button#copy-button.copied { color: #30d158; } }
476
- #output-container {
477
- background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px;
478
- min-height: 60px; border: 1px solid var(--border-color); word-wrap: break-word;
479
- font-size: 15px; color: var(--text-color); line-height: 1.5;
480
- transition: border-color 0.2s ease, background-color 0.2s ease;
481
- display: flex; align-items: center; justify-content: center;
482
- }
483
- #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; }
484
  #output-container a:hover { text-decoration: underline; }
485
- #output-container.loading::before {
486
- content: "Генерация сайта..."; display: block; text-align: center;
487
- font-style: italic; color: var(--secondary-text-color); animation: fadePulse 1.8s infinite ease-in-out;
488
- }
489
- #output-container.error {
490
- color: var(--error-color); font-weight: 500;
491
- border-color: color-mix(in srgb, var(--error-color) 50%, transparent);
492
- background-color: color-mix(in srgb, var(--error-color) 10%, var(--input-bg));
493
- justify-content: flex-start;
494
- }
495
- @keyframes fadePulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } }
496
- @media (max-width: 620px) {
497
- body { padding: 15px env(safe-area-inset-top) 15px env(safe-area-inset-bottom); align-items: flex-start; }
498
- .container { padding: 20px 20px 25px 20px; margin-top: 15px; border-radius: 20px; width: calc(100% - 30px); }
499
- h1 { font-size: 28px; }
500
- p.subtitle { font-size: 16px; margin-bottom: 25px; }
501
- .form-group { margin-bottom: 22px; }
502
- textarea#prompt-input { padding: 12px 15px; min-height: 100px; }
503
- button#generate-button { padding: 15px; font-size: 16px; }
504
- #output-container { padding: 15px 18px; font-size: 14px; min-height: 50px; }
505
- .output-section { margin-top: 30px; }
506
- }
507
  </style>
508
  </head>
509
  <body>
510
  <div class="container">
511
  <h1>EVA</h1>
512
- <p class="subtitle">Генератор Python Flask сайтов</p>
 
 
 
 
 
 
 
 
 
513
 
514
  <form id="generate-form">
515
  <div class="form-group">
516
- <label for="prompt-input" class="input-label">Опишите сайт, который вы хотите создать (на Python/Flask)</label>
517
- <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай простой одностраничный сайт-визитку на Flask. На главной странице должен быть заголовок 'Мой Сайт', текст 'Добро пожаловать!' и текущее время."></textarea>
518
  </div>
519
- <button type="submit" id="generate-button">Создать сайт</button>
520
  </form>
521
 
522
  <div class="output-section">
523
- <div class="output-header">
524
- <label id="output-label">Ссылка для запуска сайта</label>
525
- <button id="copy-button">Копировать</button>
526
- </div>
527
  <div id="output-container" aria-live="polite"></div>
528
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  </div>
530
 
531
  <script>
@@ -533,94 +560,84 @@ html_template = """
533
  const promptInput = document.getElementById('prompt-input');
534
  const outputContainer = document.getElementById('output-container');
535
  const generateButton = document.getElementById('generate-button');
536
- const copyButton = document.getElementById('copy-button');
537
 
538
  form.addEventListener('submit', async (event) => {
539
  event.preventDefault();
540
  if (!promptInput.value.trim()) {
541
- showError("Пожалуйста, опишите сайт, который вы хотите создать.");
 
542
  return;
543
  }
544
  const formData = new FormData(form);
545
  generateButton.disabled = true;
546
  generateButton.textContent = 'Генерация...';
547
  outputContainer.innerHTML = '';
548
- outputContainer.classList.add('loading');
549
- outputContainer.classList.remove('error');
550
- copyButton.style.display = 'none';
551
- copyButton.textContent = 'Копировать';
552
- copyButton.classList.remove('copied');
553
 
554
  try {
555
- const response = await fetch('/generate', { method: 'POST', body: formData });
 
 
 
556
  const result = await response.json();
557
 
558
  if (!response.ok) {
559
  throw new Error(result.error || `Ошибка сервера: ${response.status}`);
560
  }
561
 
562
- if (result.site_run_url) { // Изменено с site_url на site_run_url
563
- const link = document.createElement('a');
564
- link.href = result.site_run_url;
565
- link.textContent = "Запустить и открыть сгенерированный сайт";
566
- link.target = "_blank"; // Откроет в новой вкладке, что приведет к редиректу
567
- outputContainer.innerHTML = '';
568
- outputContainer.appendChild(link);
569
- copyButton.style.display = 'block';
570
- copyButton.dataset.copyText = window.location.origin + result.site_run_url;
 
 
 
571
  } else if (result.error) {
572
- showError(result.error);
 
573
  } else {
574
- showError("Не удалось получить ссылку на сайт. Ответ сервера не содержит URL.");
 
575
  }
576
  } catch (error) {
577
  console.error("Fetch Error:", error);
578
- showError(`Ошибка: ${error.message}`);
 
579
  } finally {
580
  generateButton.disabled = false;
581
- generateButton.textContent = 'Создать сайт';
582
- outputContainer.classList.remove('loading');
 
 
583
  }
584
  });
585
-
586
- copyButton.addEventListener('click', () => {
587
- const textToCopy = copyButton.dataset.copyText;
588
- if (!textToCopy) return;
589
- navigator.clipboard.writeText(textToCopy).then(() => {
590
- copyButton.textContent = 'Скопировано!';
591
- copyButton.classList.add('copied');
592
- setTimeout(() => {
593
- copyButton.textContent = 'Копировать';
594
- copyButton.classList.remove('copied');
595
- }, 1500);
596
- }).catch(err => {
597
- console.error('Ошибка копирования: ', err);
598
- copyButton.textContent = 'Ошибка';
599
- setTimeout(() => { copyButton.textContent = 'Копировать'; }, 1500);
600
- });
601
- });
602
-
603
- function showError(message) {
604
- outputContainer.innerHTML = '';
605
- const errorMessageElement = document.createElement('span');
606
- errorMessageElement.textContent = message;
607
- outputContainer.appendChild(errorMessageElement);
608
- outputContainer.classList.add('error');
609
- outputContainer.classList.remove('loading');
610
- copyButton.style.display = 'none';
611
- }
612
  </script>
613
  </body>
614
  </html>
615
  """
616
 
617
- # --- Маршруты Flask основного приложения ---
618
  @app.route('/')
619
  def index():
620
- return Response(html_template, mimetype='text/html')
 
 
 
 
 
 
 
 
 
 
 
621
 
622
  @app.route('/generate', methods=['POST'])
623
- def handle_generate():
624
  if 'prompt' not in request.form:
625
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
626
 
@@ -629,77 +646,109 @@ def handle_generate():
629
  return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
630
 
631
  try:
632
- python_code = generate_full_python_site_from_prompt(user_prompt)
633
 
634
  if not python_code or not python_code.strip():
635
  return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
636
 
637
- # Генерируем уникальное имя файла для .py сайта
638
- site_filename = f"site_{uuid.uuid4().hex[:12]}.py"
639
- local_filepath = os.path.join(SITES_CACHE_DIR, site_filename)
640
 
641
- with open(local_filepath, "w", encoding="utf-8") as f:
642
  f.write(python_code)
643
- logging.info(f"Сгенерированный сайт сохранен локально: {local_filepath}")
644
-
645
- # Загружаем на Hugging Face
646
- repo_filepath = f"sites/{site_filename}" # Путь в репозитории HF
647
- if upload_file_to_hf(local_filepath, repo_filepath):
648
- logging.info(f"Сайт {site_filename} успешно загружен на Hugging Face: {REPO_ID}/{repo_filepath}")
649
- else:
650
- # Не фатально, сайт все еще может работать из локального кэша, но не будет синхронизирован
651
- logging.warning(f"Не удалось загрузить сайт {site_filename} на Hugging Face.")
652
-
653
-
654
- # Ссылка для запуска сайта (которая приведет к редиректу на его порт)
655
- site_run_url = url_for('run_generated_site', filename=site_filename, _external=False)
656
- return jsonify({"site_run_url": site_run_url})
 
 
 
 
 
 
 
 
 
 
 
 
 
657
 
658
- except ValueError as ve: # Ошибки валидации или от GenAI
659
- logging.error(f"ValueError during site generation: {str(ve)}")
660
- return jsonify({"error": str(ve)}), 400 # 400 Bad Request или 422 Unprocessable Entity
 
 
 
 
 
 
 
 
 
 
 
 
661
  except Exception as e:
662
- logging.error(f"Unexpected error during site generation: {e}", exc_info=True)
663
- return jsonify({"error": f"Внутренняя ошибка сервера при генерации сайта: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
- @app.route('/run_site/<filename>')
666
- def run_generated_site(filename):
667
- if not filename.endswith(".py"):
668
- return "Неверное имя файла сайта.", 400
669
 
670
- logging.info(f"Запрос на запуск сайта: {filename}")
671
- site_host, site_port = start_generated_site(filename)
672
-
673
- if site_host and site_port:
674
- # Формируем URL для редиректа. request.host содержит <host>:<port> основного приложения.
675
- # Нам нужен только хост.
676
- main_app_host = request.host.split(':')[0]
677
- # Если основной сервер слушает на 0.0.0.0, браузер обычно обращается через localhost или конкретный IP.
678
- # Используем main_app_host, чтобы редирект был на тот же хост, с которого пришел запрос.
679
- # Если site_host это '127.0.0.1', а main_app_host это '0.0.0.0' или реальный IP,
680
- # редирект на 127.0.0.1 может сработать только локально.
681
- # Для простоты, если main_app_host '0.0.0.0', заменим на '127.0.0.1' для URL редиректа.
682
- if main_app_host == '0.0.0.0':
683
- redirect_host = '127.0.0.1'
684
- else:
685
- redirect_host = main_app_host
 
686
 
687
- redirect_url = f"http://{redirect_host}:{site_port}/"
688
- logging.info(f"Редирект на запущенный сайт: {redirect_url}")
689
- return redirect(redirect_url, code=302)
690
  else:
691
- logging.error(f"Не удалось запустить сайт {filename}.")
692
- # Можно вернуть страницу с ошибкой
693
- return f"Не удалось запустить сайт {filename}. Проверьте логи сервера.", 500
694
 
695
-
696
- if __name__ == '__main__':
697
- # Проверка наличия HF_TOKEN_WRITE для загрузки на HF
698
- if not HF_TOKEN_WRITE:
699
- logging.warning("Переменная окружения HF_TOKEN (с правом записи) не установлена. "
700
- "Сгенерированные сайты не будут загружаться на Hugging Face.")
701
-
702
- # Порт для основного приложения-генератора
703
- main_app_port = 7860
704
- logging.info(f"Основное приложение генератора запускается на порту {main_app_port}")
705
- app.run(host='0.0.0.0', port=main_app_port, debug=False)
 
1
  import os
2
  import uuid
3
+ from flask import Flask, request, jsonify, Response, send_from_directory, render_template_string, redirect, url_for, flash
4
+ import google.generativeai as genai
5
+ import json
6
  import logging
7
+ import threading
8
+ import time
9
  from datetime import datetime
10
+ from huggingface_hub import HfApi, hf_hub_download, delete_files
 
 
11
  from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError
12
+ from werkzeug.utils import secure_filename
13
+ from dotenv import load_dotenv
14
+ import subprocess
15
+ import sys
16
+ import atexit
17
+
18
+ load_dotenv()
19
 
 
20
  app = Flask(__name__)
21
+ app.secret_key = os.getenv("FLASK_SECRET_KEY", "super_secret_key_for_eva_generator")
22
 
23
+ # --- Configuration from Code 1 (EVA - AI Site Generator) ---
24
+ API_KEY_INTERNAL = os.getenv("GOOGLE_API_KEY", "AIzaSyArKidc4o0MwbaCFKStlb2q2AwNg6Pnqns") # Replace with your actual key or use env
25
+ GENERATED_APPS_DIR = 'generated_python_apps'
26
 
27
+ # --- Configuration from Code 2 (Meka Shop - Backup/Sync) ---
28
+ REPO_ID = "Kgshop/testsynk"
29
+ HF_TOKEN_WRITE = os.getenv("HF_TOKEN_WRITE")
30
+ HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") # Can be same as write token
31
+
32
+ APPS_MANIFEST_FILENAME = "apps_manifest.json"
33
+ HF_APPS_DIR_IN_REPO = "apps" # Directory in Hugging Face repo for .py files
34
 
35
  DOWNLOAD_RETRIES = 3
36
  DOWNLOAD_DELAY = 5
37
+ BACKUP_INTERVAL_SECONDS = 1800 # 30 minutes
38
 
 
 
 
 
 
 
39
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
40
 
41
+ if not os.path.exists(GENERATED_APPS_DIR):
42
+ os.makedirs(GENERATED_APPS_DIR)
 
43
 
44
+ # In-memory store for running app subprocesses
45
+ # Key: app_id, Value: {"process": Popen_object, "port": port, "filename": filename, "status": "running"}
46
+ running_apps_processes = {}
47
+ # Port assignment
48
+ next_available_port = int(os.getenv("START_PORT_GENERATED_APPS", 7861))
 
49
 
50
+ # --- Hugging Face Sync Logic (Adapted from Code 2) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ def _get_hf_token(write_access=False):
53
+ if write_access:
54
+ return HF_TOKEN_WRITE
55
+ return HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ def upload_to_hf(local_path, path_in_repo, commit_message=None):
58
  if not HF_TOKEN_WRITE:
59
+ logging.warning(f"HF_TOKEN_WRITE not set. Skipping upload of {local_path} to HF.")
60
  return False
61
+ if not os.path.exists(local_path):
62
+ logging.warning(f"Local file {local_path} not found. Skipping upload.")
63
  return False
64
+
65
+ api = HfApi()
66
+ if not commit_message:
67
+ commit_message = f"Sync {os.path.basename(local_path)} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
68
  try:
69
+ logging.info(f"Uploading {local_path} to {REPO_ID}/{path_in_repo}...")
 
70
  api.upload_file(
71
+ path_or_fileobj=local_path,
72
  path_in_repo=path_in_repo,
73
  repo_id=REPO_ID,
74
  repo_type="dataset",
75
  token=HF_TOKEN_WRITE,
76
+ commit_message=commit_message
77
  )
78
+ logging.info(f"Successfully uploaded {local_path} to HF.")
79
  return True
80
  except Exception as e:
81
+ logging.error(f"Error uploading {local_path} to Hugging Face: {e}")
82
  return False
83
 
84
+ def download_from_hf(filename_in_repo, local_dir=".", force_download=True, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
85
+ token_to_use = _get_hf_token()
86
+ if not token_to_use:
87
+ logging.warning(f"No HF token for reading. Download of {filename_in_repo} might fail for private repos.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ success = False
90
+ local_path_to_save = os.path.join(local_dir, os.path.basename(filename_in_repo))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ for attempt in range(retries + 1):
93
+ try:
94
+ logging.info(f"Downloading {filename_in_repo} from {REPO_ID} (Attempt {attempt + 1})...")
95
+ # Ensure local directory for the file exists
96
+ os.makedirs(os.path.dirname(local_path_to_save), exist_ok=True)
97
 
98
+ downloaded_path = hf_hub_download(
99
+ repo_id=REPO_ID,
100
+ filename=filename_in_repo,
101
+ repo_type="dataset",
102
+ token=token_to_use,
103
+ local_dir=local_dir, # hf_hub_download will place it correctly based on filename_in_repo structure if local_dir is base
104
+ local_dir_use_symlinks=False,
105
+ force_download=force_download,
106
+ resume_download=False
107
+ )
108
+ # hf_hub_download returns the full path, ensure it's where we expect or move it
109
+ # For simple case, if filename_in_repo is just a name, it's downloaded to local_dir/filename_in_repo
110
+ # If filename_in_repo includes slashes, it respects that structure inside local_dir
111
+ # So, downloaded_path should be local_path_to_save if filename_in_repo doesn't have internal dirs
112
+ # Let's verify by checking if the expected file exists after download
113
+ # The path hf_hub_download actually uses:
114
+ actual_downloaded_file = os.path.join(local_dir, filename_in_repo)
115
+ if os.path.exists(actual_downloaded_file):
116
+ logging.info(f"Successfully downloaded {filename_in_repo} to {actual_downloaded_file}.")
117
+ success = True
118
+ return actual_downloaded_file # Return the actual path
119
+ else:
120
+ logging.error(f"File {filename_in_repo} reported as downloaded by hf_hub, but not found at {actual_downloaded_file}.")
121
+ # This case might happen if filename_in_repo had path segments. hf_hub_download creates them.
122
+
123
+ success = True # if no error from hf_hub_download
124
+ break
125
+ except HfHubHTTPError as e:
126
+ if e.response.status_code == 404:
127
+ logging.warning(f"File {filename_in_repo} not found in repo {REPO_ID} (404).")
128
+ return None # File not found is a definitive result
129
+ logging.error(f"HTTP error downloading {filename_in_repo} (Attempt {attempt + 1}): {e}")
130
+ except Exception as e:
131
+ logging.error(f"Unexpected error downloading {filename_in_repo} (Attempt {attempt + 1}): {e}")
132
 
133
+ if attempt < retries:
134
+ time.sleep(delay)
 
 
 
135
 
136
+ if success:
137
+ # The file is downloaded directly into the structure matching path_in_repo within local_dir
138
+ # So GENERATED_APPS_DIR/app_xyz.py or ./apps_manifest.json
139
+ final_path = os.path.join(local_dir, filename_in_repo)
140
+ if os.path.exists(final_path):
141
+ return final_path
142
+ else: # Fallback if pathing is tricky with hf_hub_download's behavior
143
+ base_filename = os.path.basename(filename_in_repo)
144
+ simple_final_path = os.path.join(local_dir, base_filename)
145
+ if os.path.exists(simple_final_path):
146
+ return simple_final_path
147
+ logging.error(f"Downloaded file {filename_in_repo} but cannot locate it finally.")
148
+ return None
149
+ else:
150
+ logging.error(f"Failed to download {filename_in_repo} after retries.")
151
+ return None
152
 
 
 
 
 
 
 
 
153
 
154
+ def load_apps_manifest():
155
+ manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME) # Manifest is in root
156
+ if os.path.exists(manifest_path):
 
 
 
 
 
 
 
 
 
 
157
  try:
158
+ with open(manifest_path, 'r', encoding='utf-8') as f:
159
+ return json.load(f)
160
+ except json.JSONDecodeError:
161
+ logging.error(f"Error decoding {APPS_MANIFEST_FILENAME}. Returning empty manifest.")
162
+ return {}
163
+ return {}
164
+
165
+ def save_apps_manifest(manifest_data):
166
+ manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME)
167
+ try:
168
+ with open(manifest_path, 'w', encoding='utf-8') as f:
169
+ json.dump(manifest_data, f, indent=4, ensure_ascii=False)
170
+ logging.info(f"{APPS_MANIFEST_FILENAME} saved successfully.")
171
+ return True
172
+ except Exception as e:
173
+ logging.error(f"Error saving {APPS_MANIFEST_FILENAME}: {e}")
174
+ return False
175
+
176
+ def sync_manifest_to_hf():
177
+ manifest_path = os.path.join(".", APPS_MANIFEST_FILENAME)
178
+ if os.path.exists(manifest_path):
179
+ upload_to_hf(manifest_path, APPS_MANIFEST_FILENAME, f"Update {APPS_MANIFEST_FILENAME}")
180
+ else:
181
+ logging.warning(f"{APPS_MANIFEST_FILENAME} not found locally to upload.")
182
 
 
183
 
184
+ def sync_app_py_file_to_hf(app_id, manifest):
185
+ if app_id not in manifest:
186
+ logging.warning(f"App ID {app_id} not in manifest. Cannot sync .py file to HF.")
187
+ return False
188
+ app_entry = manifest[app_id]
189
+ local_py_path = os.path.join(GENERATED_APPS_DIR, app_entry['filename'])
190
+ hf_py_path = f"{HF_APPS_DIR_IN_REPO}/{app_entry['filename']}"
191
+ if os.path.exists(local_py_path):
192
+ return upload_to_hf(local_py_path, hf_py_path, f"Update app: {app_entry['filename']}")
193
+ return False
194
 
195
+ def initial_sync_from_hf():
196
+ logging.info("Starting initial sync from Hugging Face...")
197
+ # 1. Download manifest
198
+ manifest_local_path = download_from_hf(APPS_MANIFEST_FILENAME, local_dir=".")
199
+ manifest = {}
200
+ if manifest_local_path and os.path.exists(manifest_local_path):
201
+ manifest = load_apps_manifest()
202
+ logging.info(f"{APPS_MANIFEST_FILENAME} loaded after download.")
203
+ else:
204
+ logging.warning(f"Could not download or load {APPS_MANIFEST_FILENAME}. Starting with empty/local manifest.")
205
+ manifest = load_apps_manifest() # Try local if download failed
206
+
207
+ # 2. Download app .py files mentioned in manifest
208
+ if manifest:
209
+ logging.info(f"Manifest loaded, checking {len(manifest)} app entries for download.")
210
+ for app_id, app_entry in manifest.items():
211
+ local_py_path = os.path.join(GENERATED_APPS_DIR, app_entry['filename'])
212
+ hf_py_path = f"{HF_APPS_DIR_IN_REPO}/{app_entry['filename']}"
213
+ if not os.path.exists(local_py_path): # Only download if not exists locally
214
+ logging.info(f"App file {app_entry['filename']} not found locally. Attempting download.")
215
+ downloaded_py_path = download_from_hf(hf_py_path, local_dir=GENERATED_APPS_DIR)
216
+ if downloaded_py_path:
217
+ logging.info(f"App {app_entry['filename']} downloaded to {downloaded_py_path}.")
218
+ else:
219
+ logging.warning(f"Failed to download app {app_entry['filename']} for app_id {app_id}.")
220
+ else:
221
+ logging.info(f"App file {app_entry['filename']} already exists locally. Skipping download.")
222
+ else:
223
+ logging.info("Manifest is empty or could not be loaded. No app .py files to download.")
224
+ logging.info("Initial sync from Hugging Face finished.")
225
+ return manifest
226
+
227
+
228
+ def periodic_backup_job():
229
+ logging.info("Periodic backup: Starting...")
230
+ manifest = load_apps_manifest()
231
+ if not manifest and not os.path.exists(APPS_MANIFEST_FILENAME): # if manifest is empty and file doesnt exist, create one
232
+ save_apps_manifest({}) # Create an empty one if it doesn't exist at all
233
+
234
+ sync_manifest_to_hf() # Upload current manifest
235
+
236
+ # Optionally, upload all local .py files if they are not on HF or changed
237
+ # For simplicity in this version, we mainly rely on upload during generation
238
+ # A more robust sync would check hashes or modification times.
239
+ # For now, let's assume if an app is in manifest, its .py should be on HF
240
+ # if it was generated and uploaded successfully.
241
+ # This periodic backup mainly ensures the manifest is up-to-date on HF.
242
+ # If you want to ensure all local py files are on HF:
243
+ for app_id, app_entry in manifest.items():
244
+ local_py_path = os.path.join(GENERATED_APPS_DIR, app_entry['filename'])
245
+ if os.path.exists(local_py_path): # If it exists locally, ensure it's on HF
246
+ # This could be more intelligent (e.g., check if already uploaded or changed)
247
+ # For now, it might re-upload, which is acceptable for a backup
248
+ logging.info(f"Periodic backup: Checking app {app_entry['filename']} for HF sync.")
249
+ sync_app_py_file_to_hf(app_id, manifest)
250
+
251
+ logging.info("Periodic backup: Finished.")
252
+
253
+ # --- AI Generation Logic (Adapted from Code 1) ---
254
+
255
+ def generate_python_app_from_prompt(user_prompt):
256
+ global API_KEY_INTERNAL
257
  try:
 
258
  genai.configure(api_key=API_KEY_INTERNAL)
259
  except Exception as e:
260
+ logging.error(f"Error configuring GenAI: {e}")
261
  raise ValueError(f"Не удалось настроить Google AI. Проблема с конфигурацией. Ошибка: {e}")
262
 
263
  if not user_prompt or not user_prompt.strip():
264
  raise ValueError("Текстовый запрос (промпт) не может быть пустым.")
265
 
266
  system_instruction = (
267
+ "You are an expert Python Flask web developer. Your task is to generate a complete, single-file Flask application "
268
+ "based on the user's request. The Python file must be fully self-contained.\n"
269
+ "This means:\n"
270
+ "1. All HTML should be returned as strings within Flask route functions, or using `render_template_string`.\n"
271
+ " For example: `return '<h1>Hello</h1>'` or `return render_template_string('<h1>{{name}}</h1>', name='World')`.\n"
272
+ "2. All CSS styles must be included within `<style>` tags inside the HTML strings.\n"
273
+ "3. All client-side JavaScript code must be included within `<script>` tags inside the HTML strings.\n"
274
+ "4. Do not use external CSS/JS files. Do not use `render_template` with separate template files.\n"
275
+ "5. If the app needs to store data, use in-memory Python dictionaries or lists. Avoid file I/O unless very simple and specified.\n"
276
+ "6. The generated Python code should be well-formed and ready to be saved as a .py file and run.\n"
277
+ "7. Ensure character encoding for HTML is UTF-8 (e.g., `<meta charset=\"UTF-8\">`).\n"
278
+ "8. The Flask app should be runnable on `0.0.0.0` and use a specific port placeholder `APP_PORT`.\n"
279
+ " The `app.run` call MUST look like this: `app.run(host='0.0.0.0', port=APP_PORT)`\n"
280
+ "9. Make sure to include `from flask import Flask, render_template_string, request, jsonify` or other necessary imports.\n"
281
+ "10. The application should be defined as `app = Flask(__name__)`.\n"
282
+ "11. Directly output ONLY the Python code starting with import statements and ending with the `if __name__ == '__main__':` block. Do not include any explanatory text, markdown formatting (like ```python), or anything else before or after the Python code itself."
283
+ "12. The app should be functional and somewhat visually appealing given the constraints."
284
+ "Example of a minimal app structure:\n"
285
+ "```python\n"
286
+ "from flask import Flask, render_template_string\n\n"
287
+ "app = Flask(__name__)\n\n"
288
+ "@app.route('/')\n"
289
+ "def home():\n"
290
+ " html_content = \"\"\"\n"
291
+ " <!DOCTYPE html><html><head><meta charset='UTF-8'><title>My App</title></head>\n"
292
+ " <body><h1>Welcome!</h1></body></html>\n"
293
+ " \"\"\"\n"
294
+ " return html_content\n\n"
295
+ "if __name__ == '__main__':\n"
296
+ " APP_PORT = 5000 # This will be replaced by the generator\n"
297
+ " app.run(host='0.0.0.0', port=APP_PORT)\n"
298
+ "```\n"
299
+ "Ensure the `APP_PORT` variable is defined before `app.run` if you are using it like `port=APP_PORT`, or directly use the placeholder `app.run(host='0.0.0.0', port=APP_PORT)` without defining it. The latter is preferred: `app.run(host='0.0.0.0', port=APP_PORT)`"
300
  )
301
+
302
  full_prompt = f"{system_instruction}\n\nUser request: \"{user_prompt}\""
303
 
304
+ response_obj = None # To store the raw response object for debugging
305
  try:
306
+ model = genai.GenerativeModel('gemini-1.0-pro-latest') # Using a more capable model for code
307
+ # model = genai.GenerativeModel('learnlm-2.0-flash-experimental') # As in original code 1
308
  response_obj = model.generate_content(full_prompt)
309
 
310
  generated_text = ""
311
+ # Handle different response structures
312
  if hasattr(response_obj, 'text') and response_obj.text:
313
  generated_text = response_obj.text
314
  elif hasattr(response_obj, 'parts') and response_obj.parts:
315
  generated_text = "".join(part.text for part in response_obj.parts if hasattr(part, 'text'))
316
 
317
  if not generated_text.strip():
318
+ # Check for blocking reasons if the response object has prompt_feedback
319
+ if hasattr(response_obj, 'prompt_feedback') and response_obj.prompt_feedback:
320
+ block_reason = getattr(response_obj.prompt_feedback, 'block_reason', None)
321
+ if block_reason:
322
+ raise ValueError(f"Генерация контента заблокирована (причина: {block_reason}). Попробуйте другой запрос.")
323
+ raise ValueError("Модель вернула пустой результат. Попробуйте изменить запрос.")
 
 
324
 
325
+ # Clean markdown ```python ... ```
326
  clean_text = generated_text.strip()
327
  if clean_text.startswith("```python"):
328
+ clean_text = clean_text[9:] # len("```python\n")
 
 
 
 
 
329
  if clean_text.endswith("```"):
330
  clean_text = clean_text[:-3]
331
+ elif clean_text.startswith("```"): # More generic markdown block
332
+ clean_text = clean_text[3:]
333
+ if clean_text.endswith("```"):
334
+ clean_text = clean_text[:-3]
335
 
336
+ generated_text = clean_text.strip()
337
+
338
+ if not "Flask(__name__)" in generated_text or not "app.run" in generated_text:
339
+ logging.warning(f"Generated text might not be a valid Flask app. Preview: {generated_text[:300]}")
340
+ # raise ValueError("Сгенерированный код не похож на Flask приложение. Проверьте результат.") # Too strict for now
341
+
342
+ if "port=APP_PORT" not in generated_text and "port = APP_PORT" not in generated_text :
343
+ logging.warning("Generated app does not contain 'port=APP_PORT'. It might not be configurable by the generator.")
344
+ # Could attempt to inject it, but risky. For now, rely on AI.
345
 
346
  return generated_text
347
 
348
  except Exception as e:
349
+ logging.error(f"Error generating content with GenAI: {e}")
350
  error_message = str(e)
351
+ # Add detailed error checks from original code 1 if needed
352
  if "API key not valid" in error_message:
353
+ raise ValueError("Внутренняя ошибка конфигурации API Google.")
354
+ # ... (other specific error messages from original code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  else:
356
+ raise ValueError(f"Ошибка при генерации Python-кода приложения: {e}")
357
+
358
+
359
+ # --- Process Management ---
360
+ def get_next_app_port():
361
+ global next_available_port
362
+ # In a real app, you'd check if port is actually free
363
+ port_to_use = next_available_port
364
+ next_available_port += 1
365
+ return port_to_use
366
+
367
+ def start_python_app(app_id, filepath, port):
368
+ global running_apps_processes
369
+ try:
370
+ # Read the generated code
371
+ with open(filepath, 'r', encoding='utf-8') as f:
372
+ code = f.read()
373
+
374
+ # Replace the port placeholder
375
+ # Ensure APP_PORT is consistently used for replacement
376
+ code = code.replace("APP_PORT", str(port))
377
+
378
+ # Overwrite the file with the new port
379
+ with open(filepath, 'w', encoding='utf-8') as f:
380
+ f.write(code)
381
+
382
+ # Start the Flask app as a subprocess
383
+ # Use sys.executable to ensure it's the same Python interpreter
384
+ process = subprocess.Popen([sys.executable, filepath], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
385
+ running_apps_processes[app_id] = {"process": process, "port": port, "filename": os.path.basename(filepath), "status": "starting"}
386
+
387
+ # Give it a moment to start or fail
388
+ time.sleep(2)
389
+ if process.poll() is None: # Still running
390
+ running_apps_processes[app_id]["status"] = "running"
391
+ logging.info(f"Python app {filepath} started on port {port} with PID {process.pid}.")
392
+ return process
393
+ else: # Process terminated quickly
394
+ stdout, stderr = process.communicate()
395
+ logging.error(f"Failed to start Python app {filepath} on port {port}. Exit code: {process.returncode}")
396
+ logging.error(f"Stdout: {stdout.decode(errors='ignore')}")
397
+ logging.error(f"Stderr: {stderr.decode(errors='ignore')}")
398
+ running_apps_processes[app_id]["status"] = "error_starting"
399
+ del running_apps_processes[app_id] # Clean up
400
+ return None
401
+ except Exception as e:
402
+ logging.error(f"Exception starting Python app {filepath} on port {port}: {e}")
403
+ if app_id in running_apps_processes: # if entry was made partially
404
+ running_apps_processes[app_id]["status"] = "error_exception"
405
+ return None
406
+
407
+ def stop_python_app(app_id):
408
+ global running_apps_processes
409
+ if app_id in running_apps_processes:
410
+ app_info = running_apps_processes[app_id]
411
+ process = app_info["process"]
412
+ try:
413
+ if process.poll() is None: # If still running
414
+ process.terminate() # or process.kill()
415
+ process.wait(timeout=5) # Wait for it to terminate
416
+ logging.info(f"Python app {app_info['filename']} (PID {process.pid}) on port {app_info['port']} terminated.")
417
+ else:
418
+ logging.info(f"Python app {app_info['filename']} was already stopped.")
419
+
420
+ app_info["status"] = "stopped"
421
+ # Keep the entry in running_apps_processes with status "stopped", or del running_apps_processes[app_id]
422
+ # For now, let's keep it to reflect it was managed
423
+ return True
424
+ except subprocess.TimeoutExpired:
425
+ logging.warning(f"Timeout trying to terminate app {app_info['filename']}. It might need to be killed manually.")
426
+ process.kill() # Force kill
427
+ app_info["status"] = "killed"
428
+ return False
429
+ except Exception as e:
430
+ logging.error(f"Error stopping Python app {app_info['filename']}: {e}")
431
+ app_info["status"] = "error_stopping"
432
+ return False
433
+ return False
434
+
435
+ def stop_all_running_apps():
436
+ logging.info("Stopping all managed Python applications...")
437
+ app_ids_to_stop = list(running_apps_processes.keys()) # Iterate over a copy
438
+ for app_id in app_ids_to_stop:
439
+ if running_apps_processes[app_id].get("status") == "running":
440
+ stop_python_app(app_id)
441
+ logging.info("Finished stopping applications.")
442
 
443
+ atexit.register(stop_all_running_apps)
444
 
445
+
446
+ # --- Flask Routes ---
447
+ html_template_for_generator = """
448
  <!DOCTYPE html>
449
  <html lang="ru">
450
  <head>
451
  <meta charset="UTF-8">
452
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
453
+ <title>EVA - Генератор Python Приложений</title>
454
  <style>
455
+ :root { /* Styles from original Code 1, slightly simplified for brevity */
456
+ --system-gray-100-light: #f2f2f7; --system-gray-50-light: #ffffff;
457
+ --system-gray-dark-100-light: #000000; --system-gray-light-75-light: #8e8e93;
458
+ --system-blue-light: #007aff; --system-blue-light-hover: #005ecf;
459
+ --system-red-light: #ff3b30; --system-separator-opaque-light: #d1d1d6;
460
+ --system-gray-100-dark: #1c1c1e; --system-gray-50-dark: #000000;
461
+ --system-gray-dark-100-dark: #ffffff; --system-gray-light-75-dark: #8e8e93;
462
+ --system-blue-dark: #0a84ff; --system-blue-dark-hover: #3b9eff;
463
+ --system-red-dark: #ff453a; --system-separator-opaque-dark: #38383a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
 
465
  @media (prefers-color-scheme: dark) {
466
  :root {
467
+ --bg-color: var(--system-gray-50-dark); --content-bg: var(--system-gray-100-dark);
468
+ --text-color: var(--system-gray-dark-100-dark); --secondary-text-color: var(--system-gray-light-75-dark);
469
+ --border-color-opaque: var(--system-separator-opaque-dark); --input-bg: var(--system-gray-100-dark);
470
+ --primary-color: var(--system-blue-dark); --primary-color-hover: var(--system-blue-dark-hover);
 
 
 
 
 
 
471
  --error-color: var(--system-red-dark);
472
  }
473
  }
 
474
  @media (prefers-color-scheme: light) {
475
  :root {
476
+ --bg-color: var(--system-gray-100-light); --content-bg: var(--system-gray-50-light);
477
+ --text-color: var(--system-gray-dark-100-light); --secondary-text-color: var(--system-gray-light-75-light);
478
+ --border-color-opaque: var(--system-separator-opaque-light); --input-bg: var(--system-gray-100-light);
479
+ --primary-color: var(--system-blue-light); --primary-color-hover: var(--system-blue-light-hover);
 
 
 
 
 
 
480
  --error-color: var(--system-red-light);
481
  }
482
  }
483
+ body { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px; background-color: var(--bg-color); color: var(--text-color); display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; }
484
+ .container { background-color: var(--content-bg); padding: 30px; border-radius: 24px; box-shadow: 0 12px 28px rgba(0,0,0,0.08); max-width: 680px; width: calc(100% - 40px); box-sizing: border-box; margin-top: 30px; }
485
+ h1 { font-size: 32px; font-weight: 700; text-align: center; margin-bottom: 8px; }
486
+ p.subtitle { font-size: 17px; color: var(--secondary-text-color); text-align: center; margin-bottom: 35px; }
487
+ textarea { 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; min-height: 120px; font-family: inherit; }
488
+ 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; margin-top: 15px; }
489
+ button:hover { background-color: var(--primary-color-hover); }
490
+ button:disabled { background-color: var(--secondary-text-color); cursor: not-allowed; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  .output-section { margin-top: 35px; }
492
+ #output-container { background-color: var(--input-bg); padding: 18px 20px; border-radius: 12px; min-height: 60px; border: 1px solid var(--border-color-opaque); word-wrap: break-word; font-size: 15px; display: flex; align-items: center; justify-content: center; }
493
+ #output-container a { color: var(--primary-color); text-decoration: none; font-weight: 500; margin: 0 10px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  #output-container a:hover { text-decoration: underline; }
495
+ #output-container.loading::before { content: "Генерация приложения..."; display: block; text-align: center; color: var(--secondary-text-color); }
496
+ #output-container.error { color: var(--error-color); font-weight: 500; justify-content: flex-start; }
497
+ .form-group { margin-bottom: 20px; }
498
+ label { display: block; font-weight: 500; margin-bottom: 8px; font-size: 15px; color: var(--secondary-text-color); }
499
+ .sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; justify-content: center; }
500
+ .sync-buttons button { width: auto; padding: 10px 15px; font-size: 14px; margin-top: 0; }
501
+ .app-list { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border-color-opaque); }
502
+ .app-item { background-color: var(--input-bg); padding: 10px 15px; border-radius: 8px; margin-bottom: 10px; font-size: 14px; }
503
+ .app-item strong {color: var(--primary-color); }
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  </style>
505
  </head>
506
  <body>
507
  <div class="container">
508
  <h1>EVA</h1>
509
+ <p class="subtitle">Генератор Python Flask Приложений</p>
510
+
511
+ <div class="sync-buttons">
512
+ <form method="POST" action="{{ url_for('force_sync_to_hf_route') }}" style="display: inline;">
513
+ <button type="submit" title="Сохранить текущее состояние (манифест и приложения) в Hugging Face">Сохранить на HF</button>
514
+ </form>
515
+ <form method="POST" action="{{ url_for('force_sync_from_hf_route') }}" style="display: inline;">
516
+ <button type="submit" title="Загрузить последнее состояние (манифест и приложения) из Hugging Face">Загрузить с HF</button>
517
+ </form>
518
+ </div>
519
 
520
  <form id="generate-form">
521
  <div class="form-group">
522
+ <label for="prompt-input">Опишите приложение, которое вы хотите создать:</label>
523
+ <textarea id="prompt-input" name="prompt" rows="6" required placeholder="Например: создай простое Flask приложение с одной страницей, которое показывает текущее время."></textarea>
524
  </div>
525
+ <button type="submit" id="generate-button">Создать приложение</button>
526
  </form>
527
 
528
  <div class="output-section">
529
+ <label>Результат генерации:</label>
 
 
 
530
  <div id="output-container" aria-live="polite"></div>
531
  </div>
532
+
533
+ <div class="app-list">
534
+ <h2>Сгенерированные Приложения</h2>
535
+ <div id="apps-display-list">
536
+ <!-- App items will be injected here by JS if needed, or server-rendered -->
537
+ {% if apps_manifest and apps_manifest|length > 0 %}
538
+ {% for app_id, app_info in apps_manifest.items() %}
539
+ <div class="app-item">
540
+ <strong>ID:</strong> {{ app_id }}<br>
541
+ <strong>Файл:</strong> {{ app_info.filename }} (Порт: {{ app_info.port or 'N/A' }})<br>
542
+ <strong>Статус:</strong> {{ running_apps_processes.get(app_id, {}).get('status', app_info.status_on_generator or 'неизвестно') }} <br>
543
+ <strong>Запрос:</strong> {{ app_info.prompt[:80] }}{% if app_info.prompt|length > 80 %}...{% endif %}<br>
544
+ {% if running_apps_processes.get(app_id, {}).get('status') == 'running' and app_info.port %}
545
+ <a href="http://localhost:{{ app_info.port }}" target="_blank">Открыть приложение</a>
546
+ {% endif %}
547
+ <a href="{{ url_for('serve_generated_app_file', filename=app_info.filename) }}">Скачать .py</a>
548
+ <!-- Add stop/start buttons here if implementing full management -->
549
+ </div>
550
+ {% endfor %}
551
+ {% else %}
552
+ <p>Пока нет сгенерированных приложений.</p>
553
+ {% endif %}
554
+ </div>
555
+ </div>
556
  </div>
557
 
558
  <script>
 
560
  const promptInput = document.getElementById('prompt-input');
561
  const outputContainer = document.getElementById('output-container');
562
  const generateButton = document.getElementById('generate-button');
 
563
 
564
  form.addEventListener('submit', async (event) => {
565
  event.preventDefault();
566
  if (!promptInput.value.trim()) {
567
+ outputContainer.textContent = "Пожалуйста, опишите приложение.";
568
+ outputContainer.className = 'error';
569
  return;
570
  }
571
  const formData = new FormData(form);
572
  generateButton.disabled = true;
573
  generateButton.textContent = 'Генерация...';
574
  outputContainer.innerHTML = '';
575
+ outputContainer.className = 'loading';
 
 
 
 
576
 
577
  try {
578
+ const response = await fetch("{{ url_for('handle_generate_python_app') }}", {
579
+ method: 'POST',
580
+ body: formData
581
+ });
582
  const result = await response.json();
583
 
584
  if (!response.ok) {
585
  throw new Error(result.error || `Ошибка сервера: ${response.status}`);
586
  }
587
 
588
+ if (result.app_id && result.filename) {
589
+ let html = \`Приложение создано (ID: \${result.app_id}): \${result.filename}\`;
590
+ if (result.app_url) {
591
+ html += \`<br><a href="\${result.app_url}" target="_blank">Открыть работающее приложение (порт \${result.port})</a>\`;
592
+ } else if (result.start_error){
593
+ html += \`<br><span style="color:red;">Ошибка запуска: \${result.start_error}</span>\`;
594
+ }
595
+ html += \`<br><a href="\${result.download_url}" target="_blank">Скачать \${result.filename}</a>\`;
596
+ outputContainer.innerHTML = html;
597
+ outputContainer.className = '';
598
+ // Optionally refresh the app list below or redirect to update it
599
+ window.location.reload(); // Simple way to refresh the list
600
  } else if (result.error) {
601
+ outputContainer.textContent = result.error;
602
+ outputContainer.className = 'error';
603
  } else {
604
+ outputContainer.textContent = "Неожиданный ответ от сервера.";
605
+ outputContainer.className = 'error';
606
  }
607
  } catch (error) {
608
  console.error("Fetch Error:", error);
609
+ outputContainer.textContent = \`Ошибка: \${error.message}\`;
610
+ outputContainer.className = 'error';
611
  } finally {
612
  generateButton.disabled = false;
613
+ generateButton.textContent = 'Создать приложение';
614
+ if (outputContainer.classList.contains('loading')) { // if not set by success/error
615
+ outputContainer.classList.remove('loading');
616
+ }
617
  }
618
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  </script>
620
  </body>
621
  </html>
622
  """
623
 
 
624
  @app.route('/')
625
  def index():
626
+ manifest = load_apps_manifest()
627
+ # Pass running_apps_processes to template for dynamic status if needed
628
+ # For simplicity, status in manifest can be 'last known status'
629
+ return render_template_string(html_template_for_generator, apps_manifest=manifest, running_apps_processes=running_apps_processes)
630
+
631
+ @app.route('/apps/<filename>')
632
+ def serve_generated_app_file(filename):
633
+ safe_filename = secure_filename(filename)
634
+ if not safe_filename.endswith(".py"):
635
+ return "Invalid file type", 400
636
+ return send_from_directory(GENERATED_APPS_DIR, safe_filename, as_attachment=True)
637
+
638
 
639
  @app.route('/generate', methods=['POST'])
640
+ def handle_generate_python_app():
641
  if 'prompt' not in request.form:
642
  return jsonify({"error": "Текстовый запрос (промпт) не найден."}), 400
643
 
 
646
  return jsonify({"error": "Текстовый запрос не может быть пустым."}), 400
647
 
648
  try:
649
+ python_code = generate_python_app_from_prompt(user_prompt)
650
 
651
  if not python_code or not python_code.strip():
652
  return jsonify({"error": "Сгенерированный Python-код пуст."}), 500
653
 
654
+ app_id = str(uuid.uuid4())
655
+ filename = f"app_{app_id[:8]}.py" # Shorter filename
656
+ filepath = os.path.join(GENERATED_APPS_DIR, filename)
657
 
658
+ with open(filepath, "w", encoding="utf-8") as f:
659
  f.write(python_code)
660
+
661
+ assigned_port = get_next_app_port()
662
+ process = start_python_app(app_id, filepath, assigned_port)
663
+
664
+ app_url = None
665
+ start_error_msg = None
666
+ current_status = "stopped_or_error"
667
+
668
+ if process:
669
+ app_url = f"http://localhost:{assigned_port}"
670
+ current_status = "running"
671
+ else: # Failed to start
672
+ start_error_msg = f"Не удалось запустить приложение {filename} на порту {assigned_port}."
673
+ current_status = "error_starting"
674
+
675
+
676
+ # Update manifest
677
+ manifest = load_apps_manifest()
678
+ manifest[app_id] = {
679
+ "filename": filename,
680
+ "prompt": user_prompt,
681
+ "port": assigned_port if process else None,
682
+ "created_at": datetime.now().isoformat(),
683
+ "status_on_generator": current_status, # "running", "error_starting", "stopped" (if we add stop UI)
684
+ "hf_path_py": f"{HF_APPS_DIR_IN_REPO}/{filename}"
685
+ }
686
+ save_apps_manifest(manifest)
687
 
688
+ # Sync to Hugging Face
689
+ sync_manifest_to_hf()
690
+ sync_app_py_file_to_hf(app_id, manifest)
691
+
692
+ return jsonify({
693
+ "app_id": app_id,
694
+ "filename": filename,
695
+ "port": assigned_port,
696
+ "app_url": app_url,
697
+ "download_url": url_for('serve_generated_app_file', filename=filename, _external=False),
698
+ "start_error": start_error_msg
699
+ })
700
+
701
+ except ValueError as ve: # From AI generation or validation
702
+ return jsonify({"error": str(ve)}), 400
703
  except Exception as e:
704
+ logging.error(f"Unexpected error during Python app generation: {e}", exc_info=True)
705
+ return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500
706
+
707
+ # --- Routes for manual sync (can be called from UI buttons) ---
708
+ @app.route('/admin/sync_to_hf', methods=['POST'])
709
+ def force_sync_to_hf_route():
710
+ logging.info("Forcing full sync to Hugging Face...")
711
+ manifest = load_apps_manifest()
712
+ sync_manifest_to_hf()
713
+ for app_id in manifest:
714
+ sync_app_py_file_to_hf(app_id, manifest)
715
+ flash("Синхронизация с Hugging Face (выгрузка) завершена.", "success")
716
+ return redirect(url_for('index'))
717
+
718
+ @app.route('/admin/sync_from_hf', methods=['POST'])
719
+ def force_sync_from_hf_route():
720
+ logging.info("Forcing full sync from Hugging Face...")
721
+ initial_sync_from_hf() # This re-downloads manifest and missing apps
722
+ # Note: This doesn't automatically (re)start downloaded apps. That would be an enhancement.
723
+ flash("Синхронизация с Hugging Face (загрузка) завершена. Перезагрузите страницу, чтобы увидеть изменения.", "info")
724
+ return redirect(url_for('index'))
725
+
726
 
727
+ if __name__ == '__main__':
728
+ # Initial sync when the app starts
729
+ initial_sync_from_hf()
 
730
 
731
+ # Start periodic backup thread if HF_TOKEN_WRITE is available
732
+ if HF_TOKEN_WRITE:
733
+ backup_thread = threading.Thread(target=lambda: {
734
+ logging.info("Periodic backup thread started."),
735
+ initial_sync_from_hf(), # Ensure we have the latest before starting periodic saves
736
+ periodic_backup_job() # First backup immediate
737
+ }, daemon=True)
738
+
739
+ def run_periodic_backup_loop():
740
+ while True:
741
+ time.sleep(BACKUP_INTERVAL_SECONDS)
742
+ periodic_backup_job()
743
+
744
+ loop_thread = threading.Thread(target=run_periodic_backup_loop, daemon=True)
745
+
746
+ backup_thread.start() # initial sync and first backup
747
+ loop_thread.start() # starts the loop
748
 
 
 
 
749
  else:
750
+ logging.warning("HF_TOKEN_WRITE not set. Periodic backup to Hugging Face will NOT run.")
 
 
751
 
752
+ main_app_port = int(os.getenv("MAIN_APP_PORT", 7860))
753
+ logging.info(f"EVA - Python App Generator starting on http://localhost:{main_app_port}")
754
+ app.run(host='0.0.0.0', port=main_app_port, debug=False) # Debug False for production/multiple processes