|
|
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({}) |
|
|
|
|
|
|
|
|
def get_user_id(request): |
|
|
"""Получает идентификатор пользователя из запроса""" |
|
|
if not request: |
|
|
return "anonymous_user" |
|
|
|
|
|
|
|
|
client_ip = request.client.host if request.client else "unknown" |
|
|
user_agent = request.headers.get("user-agent", "unknown") |
|
|
|
|
|
|
|
|
session_str = f"{client_ip}_{user_agent}" |
|
|
return hashlib.md5(session_str.encode()).hexdigest() |
|
|
|
|
|
|
|
|
def update_evolution_map(num_completed): |
|
|
html = '<div class="evolution-map-container">' |
|
|
stages = ["🌱", "🌿", "🪴", "🌳", "🌟"] |
|
|
for i in range(5): |
|
|
if i < num_completed: |
|
|
html += f'<div class="evolution-stage completed">{stages[i]}</div>' |
|
|
else: |
|
|
html += f'<div class="evolution-stage">{stages[i]}</div>' |
|
|
html += '</div>' |
|
|
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 |
|
|
|
|
|
|
|
|
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("### 🎯 Твой микро-импульс на сегодня") |
|
|
|
|
|
|
|
|
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"] |
|
|
) |
|
|
|
|
|
|
|
|
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); |
|
|
""" |
|
|
|
|
|
|
|
|
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) |