import gradio as gr import random import json import os import hashlib from datetime import datetime, timedelta # Путь к файлу с данными пользователей DATA_FILE = "user_data.json" # База данных импульсов MICRO_IMPULSES = [ {"task": "Закрой глаза. Сосредоточься на вдохе и выдохе. 1 минута.", "pillar": "Сознание"}, {"task": "Возьми ручку. Напиши слово, которое пришло в голову. Сделай из него стихотворение из 3 слов.", "pillar": "Творчество"}, {"task": "Напиши короткое 'Привет' человеку, с которым давно не общался.", "pillar": "Связи"}, {"task": "Встань. Сделай 10 приседаний. Не откладывай.", "pillar": "Здоровье"}, {"task": "Выпей стакан воды. Почувствуй, как ты заботишься о себе.", "pillar": "Здоровье"}, {"task": "Сделай 5 глубоких вдохов и выдохов, сосредоточившись только на дыхании.", "pillar": "Сознание"}, {"task": "Напиши 3 вещи, за которые ты благодарен прямо сейчас.", "pillar": "Сознание"}, {"task": "Потянись всем телом в течение 30 секунд.", "pillar": "Здоровье"}, {"task": "Улыбнись своему отражению в зеркале.", "pillar": "Связи"}, {"task": "Выдели 5 минут, чтобы просто посидеть в тишине.", "pillar": "Сознание"}, {"task": "Запиши одну маленькую идею, которая пришла тебе в голову.", "pillar": "Творчество"}, {"task": "Отправь кому-то картинку, которая вызвала у тебя улыбку.", "pillar": "Связи"}, {"task": "Сделай короткую прогулку (хотя бы 5 минут).", "pillar": "Здоровье"}, {"task": "Напиши комплимент самому себе.", "pillar": "Сознание"}, {"task": "Придумай альтернативное использование для обычного предмета.", "pillar": "Творчество"}, ] # Загрузка и сохранение данных def load_user_data(): try: if os.path.exists(DATA_FILE): with open(DATA_FILE, "r", encoding="utf-8") as f: return json.load(f) return {} except Exception: return {} def save_user_data(data): try: with open(DATA_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) return True except Exception: return False if not os.path.exists(DATA_FILE): save_user_data({}) # Функция для получения user_id из запроса def get_user_id(request): """Получает идентификатор пользователя из запроса""" if not request: return "anonymous_user" # Используем комбинацию IP и User-Agent для создания уникального ID client_ip = request.client.host if request.client else "unknown" user_agent = request.headers.get("user-agent", "unknown") # Создаем стабильный ID на основе IP и User-Agent session_str = f"{client_ip}_{user_agent}" return hashlib.md5(session_str.encode()).hexdigest() # Функции для работы с пользовательскими данными def update_evolution_map(num_completed): html = '
' stages = ["🌱", "🌿", "🪴", "🌳", "🌟"] for i in range(5): if i < num_completed: html += f'
{stages[i]}
' else: html += f'
{stages[i]}
' html += '
' return html def load_user_state(request: gr.Request): user_id = get_user_id(request) user_data = load_user_data() user_info = user_data.get(user_id, {}) completed_tasks = user_info.get("completed_tasks", []) updated_map_html = update_evolution_map(len(completed_tasks)) completed_tasks_display_str = "\n".join(completed_tasks) if completed_tasks else "" return completed_tasks_display_str, updated_map_html def get_random_impulse(request: gr.Request): user_id = get_user_id(request) user_data = load_user_data() user_info = user_data.get(user_id, {}) last_execution_time_raw = user_info.get("last_execution_time", {}) last_execution_time = {} for task, time_str in last_execution_time_raw.items(): try: last_execution_time[task] = datetime.fromisoformat(time_str) except: pass now = datetime.now() available_impulses = [] for impulse in MICRO_IMPULSES: task = impulse['task'] if task in last_execution_time: if (now - last_execution_time[task]) <= timedelta(hours=12): continue available_impulses.append(impulse) if not available_impulses: return "🎯 Отлично! Ты выполнил все доступные импульсы. Новые появятся через 12 часов." selected_impulse = random.choice(available_impulses) return selected_impulse['task'] def complete_impulse(current_task, request: gr.Request): if not current_task or current_task in ["Нажми 'Сгенерировать импульс'", "🎯 Отлично! Ты выполнил все доступные импульсы. Новые появятся через 12 часов."]: return "❌ Сначала сгенерируй импульс", "", update_evolution_map(0) user_id = get_user_id(request) user_data = load_user_data() user_info = user_data.get(user_id, {}) completed_tasks = user_info.get("completed_tasks", []) last_execution_time_raw = user_info.get("last_execution_time", {}) last_execution_time = {} for task, time_str in last_execution_time_raw.items(): try: last_execution_time[task] = datetime.fromisoformat(time_str) except: pass completed_tasks.append(f"✅ {datetime.now().strftime('%H:%M')} - {current_task}") last_execution_time[current_task] = datetime.now() last_execution_time_str = {k: v.isoformat() for k, v in last_execution_time.items()} user_data[user_id] = { "completed_tasks": completed_tasks, "last_execution_time": last_execution_time_str } save_user_data(user_data) updated_map_html = update_evolution_map(len(completed_tasks)) completed_tasks_display_str = "\n".join(completed_tasks) return "🎉 Отлично! Ещё один шаг к лучшей версии себя!", completed_tasks_display_str, updated_map_html def reset_tasks(request: gr.Request): user_id = get_user_id(request) user_data = load_user_data() user_data[user_id] = { "completed_tasks": [], "last_execution_time": {} } save_user_data(user_data) updated_map_html = update_evolution_map(0) completed_tasks_display_str = "" return "🔄 Прогресс сброшен. Начни новую эволюцию!", completed_tasks_display_str, updated_map_html def add_impulse(task, pillar): if not task or not pillar: return "❌ Введите задачу и категорию" MICRO_IMPULSES.append({"task": task, "pillar": pillar}) return f"✅ Импульс добавлен: '{task}'" def delete_impulse(task_to_remove, request: gr.Request): if not task_to_remove: return "❌ Введите задачу для удаления", "", update_evolution_map(0) global MICRO_IMPULSES initial_count = len(MICRO_IMPULSES) MICRO_IMPULSES = [imp for imp in MICRO_IMPULSES if imp['task'] != task_to_remove] if len(MICRO_IMPULSES) == initial_count: return "❌ Импульс не найден", "", update_evolution_map(0) user_id = get_user_id(request) user_data = load_user_data() if user_id in user_data: user_info = user_data[user_id] completed_tasks = [t for t in user_info.get("completed_tasks", []) if task_to_remove not in t] last_execution_time = user_info.get("last_execution_time", {}) if task_to_remove in last_execution_time: del last_execution_time[task_to_remove] user_data[user_id] = { "completed_tasks": completed_tasks, "last_execution_time": last_execution_time } save_user_data(user_data) updated_map_html = update_evolution_map(len(completed_tasks)) completed_tasks_display_str = "\n".join(completed_tasks) return f"✅ Импульс удалён", completed_tasks_display_str, updated_map_html updated_map_html = update_evolution_map(0) return "✅ Импульс удалён из базы", "", updated_map_html # Адаптивный CSS для мобильных устройств с расширяющимся полем импульсов custom_css = """ /* Базовые стили */ .gradio-container { font-family: 'Arial', sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 10px; } .main-block { background: white; border-radius: 15px; padding: 20px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); margin: 0 auto; max-width: 100%; width: 100%; box-sizing: border-box; } .app-title { text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 1.8em !important; font-weight: 700 !important; margin-bottom: 10px !important; line-height: 1.3; } .app-subtitle { text-align: center; color: #666; font-size: 1em !important; margin-bottom: 20px !important; font-style: italic; line-height: 1.4; } /* АДАПТИВНОЕ ПОЛЕ ИМПУЛЬСА - КЛЮЧЕВЫЕ ИЗМЕНЕНИЯ */ .task-display-container { width: 100%; margin-bottom: 15px; } .task-display { font-size: 1.1em !important; font-weight: 600 !important; text-align: center; background: white; border: 2px solid #667eea; border-radius: 10px; padding: 15px; margin-bottom: 0; min-height: 60px; display: flex; align-items: center; justify-content: center; word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; resize: none; width: 100%; box-sizing: border-box; line-height: 1.4; /* Автоматическая высота */ height: auto; min-height: 80px; max-height: none; } /* Убираем стандартные стили текстового поля */ .task-display textarea { resize: none !important; height: auto !important; min-height: 80px !important; } /* Специальные стили для контейнера текстового поля */ .task-display .wrap { height: auto !important; min-height: 80px !important; } /* Кнопки */ .action-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; padding: 12px 15px !important; font-weight: 600 !important; margin: 5px !important; width: 100%; font-size: 0.9em; } .action-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4) !important; } .button-row { display: flex; flex-direction: column; gap: 8px; width: 100%; } .reset-button { background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; padding: 12px 15px !important; font-weight: 600 !important; margin: 5px 0 !important; width: 100%; font-size: 0.9em; } .add-button, .delete-button { background: linear-gradient(135deg, #4ecdc4 0%, #44a08d 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; padding: 12px 15px !important; font-weight: 600 !important; margin: 5px 0 !important; width: 100%; font-size: 0.9em; } .delete-button { background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%) !important; } /* Карта эволюции */ .evolution-map-container { display: flex; gap: 10px; margin: 15px 0; justify-content: center; align-items: center; flex-wrap: wrap; } .evolution-stage { width: 40px; height: 40px; border-radius: 50%; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999; font-size: 16px; flex-shrink: 0; } .evolution-stage.completed { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: bold; box-shadow: 0 3px 10px rgba(0,0,0,0.2); } /* Список задач */ .completed-tasks-display { background: white; border-radius: 10px; border: 2px solid #e9ecef; padding: 12px; font-size: 0.95em; min-height: 150px; width: 100%; box-sizing: border-box; white-space: pre-wrap; overflow-wrap: break-word; } /* Заголовки */ .map-title, .quote-title { text-align: center; color: #333; font-size: 1.2em !important; font-weight: 600 !important; margin-bottom: 12px !important; } .quote-text { text-align: center; font-style: italic; color: #666; font-size: 1em; background: #f8f9fa; padding: 12px; border-radius: 8px; border-left: 4px solid #667eea; margin: 10px 0; } /* Аккордеоны */ .accordion-content { width: 100%; } .accordion-input { width: 100%; margin-bottom: 10px; } /* Адаптивность для десктопов */ @media (min-width: 768px) { .gradio-container { padding: 20px; } .main-block { max-width: 800px; padding: 30px; border-radius: 20px; } .app-title { font-size: 2.5em !important; } .app-subtitle { font-size: 1.2em !important; } .task-display { font-size: 1.3em !important; padding: 20px; min-height: 100px; } .button-row { flex-direction: row; justify-content: space-between; } .action-button, .reset-button, .add-button, .delete-button { width: auto; flex: 1; margin: 5px !important; } .evolution-stage { width: 50px; height: 50px; font-size: 20px; } .completed-tasks-display { font-size: 1.1em; padding: 15px; } .map-title, .quote-title { font-size: 1.4em !important; } .quote-text { font-size: 1.2em; padding: 15px; } } /* Особые стили для очень маленьких экранов */ @media (max-width: 360px) { .gradio-container { padding: 5px; } .main-block { padding: 15px; border-radius: 10px; } .app-title { font-size: 1.5em !important; } .evolution-stage { width: 35px; height: 35px; font-size: 14px; } .task-display { font-size: 1em !important; padding: 12px; min-height: 70px; } } /* Улучшения для элементов формы */ input, textarea { border-radius: 8px !important; padding: 12px !important; font-size: 16px !important; /* Предотвращает масштабирование в iOS */ } /* Убираем горизонтальную прокрутку */ body, .gradio-container { max-width: 100%; overflow-x: hidden; } /* Улучшаем отображение аккордеонов на мобильных */ .gr-accordion { width: 100% !important; } .gr-accordion-item { width: 100% !important; } /* Специальные стили для автоматического расширения текстовых полей */ .auto-expand { height: auto !important; min-height: 80px !important; resize: none !important; } /* Убираем ограничение по высоте для текстовых областей */ .gr-box { height: auto !important; min-height: 80px !important; } """ # Создание адаптивного интерфейса with gr.Blocks(title="Luka - Тренер от Прокрастинации", css=custom_css) as demo: with gr.Column(elem_classes=["main-block"]): gr.Markdown(""" # 🌱 Luka - Тренер от Прокрастинации *Преодолевай паралич действий. Становись кем-то новым — шаг за шагом.* """, elem_classes=["app-title"]) gr.Markdown("### 🎯 Твой микро-импульс на сегодня") # Используем Textarea вместо Textbox для лучшего контроля над высотой task_display = gr.Textbox( label="", value="Нажми 'Сгенерировать импульс' 👇", interactive=False, elem_classes=["task-display"], lines=2, # Начальное количество строк max_lines=10, # Максимальное количество строк show_copy_button=False ) with gr.Row(elem_classes=["button-row"]): generate_btn = gr.Button("🎲 Сгенерировать импульс", elem_classes=["action-button"]) complete_btn = gr.Button("✅ Выполнено", elem_classes=["action-button"]) gr.Markdown("### 📊 Твой прогресс") evolution_map_html = gr.HTML(update_evolution_map(0)) with gr.Column(): completed_tasks_display = gr.Textbox( label="📝 Выполненные задачи", interactive=False, lines=6, elem_classes=["completed-tasks-display"] ) reset_btn = gr.Button("🔄 Сбросить прогресс", elem_classes=["reset-button"]) with gr.Row(): with gr.Column(): with gr.Accordion("➕ Добавить свой импульс", open=False, elem_classes=["accordion-content"]): new_task_input = gr.Textbox( label="Задача", placeholder="Опиши микро-действие...", elem_classes=["accordion-input"] ) new_pillar_input = gr.Textbox( label="Категория", placeholder="Сознание/Здоровье/Творчество/Связи", elem_classes=["accordion-input"] ) add_impulse_btn = gr.Button("Добавить импульс", elem_classes=["add-button"]) add_output = gr.Textbox(label="", interactive=False) with gr.Column(): with gr.Accordion("🗑️ Удалить импульс", open=False, elem_classes=["accordion-content"]): task_to_delete = gr.Textbox( label="Текст задачи для удаления", placeholder="Введите точный текст...", elem_classes=["accordion-input"] ) delete_btn = gr.Button("Удалить импульс", elem_classes=["delete-button"]) delete_output = gr.Textbox(label="", interactive=False) gr.Markdown("### 💫 Ты становишься человеком, который...", elem_classes=["quote-title"]) gr.Textbox( value="...делает один маленький шаг каждый день к своей лучшей версии", interactive=False, elem_classes=["quote-text"] ) # JavaScript для автоматического изменения высоты текстового поля js_auto_resize = """ function autoResizeTextarea() { setTimeout(function() { const textareas = document.querySelectorAll('.task-display textarea'); textareas.forEach(function(textarea) { // Сбрасываем высоту чтобы получить правильный scrollHeight textarea.style.height = 'auto'; // Устанавливаем высоту based on scrollHeight textarea.style.height = textarea.scrollHeight + 'px'; }); }, 100); } // Вызываем при загрузке и при изменении содержимого document.addEventListener('DOMContentLoaded', autoResizeTextarea); // Создаем наблюдатель за изменениями DOM const observer = new MutationObserver(autoResizeTextarea); observer.observe(document.body, { childList: true, subtree: true }); // Также вызываем при изменении размера окна window.addEventListener('resize', autoResizeTextarea); """ # Обработчики событий с передачей request demo.load( fn=load_user_state, inputs=None, outputs=[completed_tasks_display, evolution_map_html], js=js_auto_resize ) generate_btn.click( fn=get_random_impulse, inputs=None, outputs=task_display, js=js_auto_resize ) complete_btn.click( fn=complete_impulse, inputs=[task_display], outputs=[task_display, completed_tasks_display, evolution_map_html], js=js_auto_resize ) reset_btn.click( fn=reset_tasks, inputs=None, outputs=[task_display, completed_tasks_display, evolution_map_html], js=js_auto_resize ) add_impulse_btn.click( fn=add_impulse, inputs=[new_task_input, new_pillar_input], outputs=add_output ).then( fn=lambda: ("", ""), outputs=[new_task_input, new_pillar_input] ) delete_btn.click( fn=delete_impulse, inputs=[task_to_delete], outputs=[delete_output, completed_tasks_display, evolution_map_html] ).then( fn=lambda: "", outputs=task_to_delete ) if __name__ == "__main__": demo.launch(show_api=False)