Luka / app.py
Aist1's picture
Update app.py
202ba75 verified
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 = '<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
# Адаптивный 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)