Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
haarburger committed Aug 24, 2024
0 parents commit c85a5a1
Show file tree
Hide file tree
Showing 2 changed files with 392 additions and 0 deletions.
86 changes: 86 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from flask import Flask, render_template, request, jsonify
import anthropic
import os
from functools import wraps
from collections import deque
import time

app = Flask(__name__)

workout_styles = ["AMRAP", "For Time", "EMOM"]
equipment = sorted(["Barbell", "Kettlebell", "Bodyweight", "Dumbbell", "Jump Rope", "Pull-up Bar", "Bike Erg"])
durations = range(2, 31)

# Rate limiting setup
RATE_LIMIT = 3
RATE_LIMIT_PERIOD = 60 # 1 minute in seconds
request_timestamps = deque()

def rate_limited(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_time = time.time()

# Remove timestamps older than the rate limit period
while request_timestamps and current_time - request_timestamps[0] > RATE_LIMIT_PERIOD:
request_timestamps.popleft()

if len(request_timestamps) >= RATE_LIMIT:
return jsonify({"error": "Rate limit exceeded. Please try again in 1 minute."}), 429

request_timestamps.append(current_time)
return func(*args, **kwargs)
return wrapper

@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
return generate_workout_handler()
return render_template('index.html', styles=workout_styles, equipment=equipment, durations=durations, loading=False)

@rate_limited
def generate_workout_handler():
style = request.form.get('style')
equip = request.form.getlist('equipment') # Changed to getlist to handle multiple selections
duration = request.form.get('duration')

workout = generate_workout(style, equip, duration)
return jsonify({"workout": workout})

def generate_workout(style, equip, duration):
import anthropic
import os

# Get the Anthropic API key from environment variables
api_key = os.environ.get('ANTHROPIC_API_KEY')
if not api_key:
raise ValueError("No API key found. Please set the ANTHROPIC_API_KEY environment variable.")

client = anthropic.Anthropic(api_key=api_key)

prompt = f"""
Human: Generate a Crossfit-style workout with the following parameters:
Style: {style}
Equipment: {', '.join(equip)}
Duration: {duration} minutes
Just post the raw workout, no preamble or other text, no warmup or cooldown. Print each exercise on a new line. Use all the equipment that was provided. Always mention the number of rounds, reps, and rest time.
Assistant:
"""

try:
response = client.completions.create(
model="claude-2",
prompt=prompt,
max_tokens_to_sample=150
)
workout = response.completion
except Exception as e:
workout = f"Error generating workout: {str(e)}"

return workout


if __name__ == '__main__':
app.run(debug=True)
306 changes: 306 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workout Generator</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.gradient-bg {
background: linear-gradient(-45deg, rgba(128, 128, 128, 0.3), rgba(128, 128, 128, 0.3), rgba(128, 128, 128, 0.3), rgba(128, 128, 128, 0.3));
background-size: 400% 400%;
}
</style>
</head>
<body class="gradient-bg flex flex-col justify-center items-center min-h-screen">
<div class="container mx-auto max-w-2xl bg-white bg-opacity-90 p-10 rounded-2xl shadow-2xl backdrop-blur-sm mb-8">
<h1 class="text-4xl font-extrabold mb-2 text-center bg-clip-text text-transparent bg-gradient-to-r from-purple-400 via-pink-500 to-red-500">Workout Generator</h1>
<p class="text-gray-600 mb-8 text-center">Create a simple Crossfit-style workout in seconds</p>
<form id="workout-form" method="POST" class="space-y-6">
<div>
<label for="style" class="block text-sm font-medium text-gray-700 mb-1">Workout Style</label>
<div class="relative">
<select name="style" id="style" class="hidden">
{% for style in styles %}
<option value="{{ style }}">{{ style }}</option>
{% endfor %}
</select>
<div id="style-dropdown" class="bg-white border-2 border-gray-300 rounded-lg p-2 cursor-pointer">
<span class="text-gray-500">Select workout style...</span>
</div>
<div id="style-options" class="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden">
{% for style in styles %}
<div class="style-option p-2 hover:bg-gray-100 cursor-pointer" data-value="{{ style }}">{{ style }}</div>
{% endfor %}
</div>
</div>
</div>
<div>
<label for="equipment" class="block text-sm font-medium text-gray-700 mb-1">Equipment</label>
<div id="selected-equipment" class="mb-2 flex flex-wrap gap-2"></div>
<div class="relative">
<select name="equipment" id="equipment" multiple class="hidden">
{% for item in equipment %}
<option value="{{ item }}">{{ item }}</option>
{% endfor %}
</select>
<div id="equipment-dropdown" class="bg-white border-2 border-gray-300 rounded-lg p-2 cursor-pointer">
<span class="text-gray-500">Select equipment...</span>
</div>
<div id="equipment-options" class="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden max-h-48 overflow-y-auto">
<div class="equipment-option p-2 hover:bg-gray-100 cursor-pointer" id="add-custom-equipment">
<input type="text" placeholder="Add custom equipment" class="w-full p-1 border border-gray-300 rounded" id="custom-equipment-input">
</div>
{% for item in equipment %}
<div class="equipment-option p-2 hover:bg-gray-100 cursor-pointer" data-value="{{ item }}">{{ item }}</div>
{% endfor %}
</div>
</div>
</div>
<div class="mb-8"> <!-- Increased bottom margin -->
<label for="duration" class="block text-sm font-medium text-gray-700 mb-1">Duration</label>
<div class="relative pt-1">
<input type="range" name="duration" id="duration" min="2" max="30" value="15"
class="w-full h-2 bg-gray-200 rounded-full appearance-none cursor-pointer"
oninput="updateSlider(this)">
<div class="absolute left-0 right-0 -bottom-6 flex justify-between">
<span class="text-xs text-gray-500">2m</span>
<span class="text-xs text-gray-500">30m</span>
</div>
<output id="duration-output" class="absolute left-1/2 transform -translate-x-1/2 -top-8 bg-indigo-600 text-white px-2 py-1 rounded-full text-sm font-semibold whitespace-nowrap">15 minutes</output>
</div>
</div>
<div class="mt-12 flex justify-center"> <!-- Added margin-top and centering -->
<button type="submit" id="generate-btn" class="px-8 py-4 bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold rounded-full hover:from-blue-600 hover:to-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-200 ease-in-out transform hover:-translate-y-1 hover:shadow-lg text-lg">
Generate Workout
</button>
</div>
</form>
</div>
<div class="container mx-auto max-w-2xl">
<div id="loading" class="hidden text-center">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-indigo-600"></div>
<p class="mt-2 text-indigo-600 font-semibold">Generating workout...</p>
</div>
<div id="workout-container">
{% if workout %}
<div id="workout-result" class="p-6 bg-white rounded-full shadow-md border border-gray-200">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Your Workout:</h2>
<div class="bg-gradient-to-br from-blue-50 to-indigo-100 p-4 rounded-full">
<pre class="text-gray-700 text-lg whitespace-pre-line">{{ workout }}</pre>
</div>
</div>
{% endif %}
</div>
</div>
<script>
function updateSlider(slider) {
const value = slider.value;
const min = slider.min;
const max = slider.max;
const percentage = (value - min) / (max - min) * 100;

slider.style.background = `linear-gradient(to right, #4F46E5 0%, #4F46E5 ${percentage}%, #E5E7EB ${percentage}%, #E5E7EB 100%)`;

const output = document.getElementById('duration-output');
output.textContent = `${value} minutes`;

// Adjust positioning to prevent overflow
const thumbWidth = 16; // Approximate width of the slider thumb
const outputWidth = output.offsetWidth;
const sliderWidth = slider.offsetWidth;

let leftPosition = (percentage / 100) * (sliderWidth - thumbWidth) + (thumbWidth / 2) - (outputWidth / 2);

// Ensure the output doesn't go beyond the slider's edges
leftPosition = Math.max(0, Math.min(leftPosition, sliderWidth - outputWidth));

output.style.left = `${leftPosition}px`;
}

document.addEventListener('DOMContentLoaded', function() {
const slider = document.getElementById('duration');
updateSlider(slider);

const equipmentDropdown = document.getElementById('equipment-dropdown');
const equipmentOptions = document.getElementById('equipment-options');
const selectedEquipment = document.getElementById('selected-equipment');
const equipmentSelect = document.getElementById('equipment');

equipmentDropdown.addEventListener('click', () => {
equipmentOptions.classList.toggle('hidden');
});

const customEquipmentInput = document.getElementById('custom-equipment-input');
const addCustomEquipment = document.getElementById('add-custom-equipment');

customEquipmentInput.addEventListener('click', (e) => {
e.stopPropagation();
});

customEquipmentInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
const customValue = this.value.trim();
if (customValue) {
addEquipment(customValue);
this.value = '';
equipmentOptions.classList.add('hidden');
}
}
});

addCustomEquipment.addEventListener('click', (e) => {
e.stopPropagation();
customEquipmentInput.focus();
});

function addEquipment(value) {
const existingOption = selectedEquipment.querySelector(`[data-value="${value}"]`);

if (!existingOption) {
const tag = document.createElement('div');
tag.className = 'bg-blue-100 text-blue-800 px-2 py-1 rounded-full text-sm flex items-center';
tag.dataset.value = value;
tag.innerHTML = `
${value}
<button class="ml-1 text-blue-600 hover:text-blue-800 focus:outline-none">
&times;
</button>
`;
tag.style.opacity = '0';
tag.style.transform = 'scale(0.8)';
selectedEquipment.appendChild(tag);

// If it's a custom value, add it to the select element
if (!equipmentSelect.querySelector(`option[value="${value}"]`)) {
const newOption = document.createElement('option');
newOption.value = value;
newOption.text = value;
equipmentSelect.appendChild(newOption);
}

equipmentSelect.querySelector(`option[value="${value}"]`).selected = true;

// Hide the option in the dropdown if it's not custom
const optionElement = document.querySelector(`.equipment-option[data-value="${value}"]`);
if (optionElement) {
optionElement.classList.add('hidden');
}

setTimeout(() => {
tag.style.transition = 'opacity 0.3s, transform 0.3s';
tag.style.opacity = '1';
tag.style.transform = 'scale(1)';
}, 10);

tag.querySelector('button').addEventListener('click', (e) => {
e.stopPropagation();
tag.style.opacity = '0';
tag.style.transform = 'scale(0.8)';
setTimeout(() => {
tag.remove();
equipmentSelect.querySelector(`option[value="${value}"]`).selected = false;
if (optionElement) {
optionElement.classList.remove('hidden');
}
}, 300);
});
}
}

document.addEventListener('click', (e) => {
if (!equipmentDropdown.contains(e.target) && !equipmentOptions.contains(e.target)) {
equipmentOptions.classList.add('hidden');
}
});

const styleDropdown = document.getElementById('style-dropdown');
const styleOptions = document.getElementById('style-options');
const styleSelect = document.getElementById('style');

styleDropdown.addEventListener('click', () => {
styleOptions.classList.toggle('hidden');
});

document.querySelectorAll('.style-option').forEach(option => {
option.addEventListener('click', () => {
const value = option.dataset.value;
styleDropdown.querySelector('span').textContent = value;
styleSelect.value = value;
styleOptions.classList.add('hidden');
});
});

document.addEventListener('click', (e) => {
if (!styleDropdown.contains(e.target) && !styleOptions.contains(e.target)) {
styleOptions.classList.add('hidden');
}
});

// Add this new event listener for existing equipment options
document.querySelectorAll('.equipment-option').forEach(option => {
option.addEventListener('click', () => {
if (option.id !== 'add-custom-equipment') {
addEquipment(option.dataset.value);
}
});
});
});

document.getElementById('workout-form').addEventListener('submit', function(e) {
e.preventDefault(); // Prevent the form from submitting normally
const generateBtn = document.getElementById('generate-btn');
const loadingIndicator = document.getElementById('loading');

generateBtn.disabled = true;
generateBtn.classList.add('opacity-50', 'cursor-not-allowed');
loadingIndicator.classList.remove('hidden');

if (document.getElementById('workout-result')) {
document.getElementById('workout-result').classList.add('hidden');
}

// Collect form data
const formData = new FormData(this);

// Send AJAX request
fetch('/', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
const workoutContainer = document.getElementById('workout-container');
workoutContainer.innerHTML = `
<div id="workout-result" class="p-6 bg-white rounded-lg shadow-md border border-gray-200">
<h2 class="text-2xl font-bold mb-4 text-gray-800">Your Workout:</h2>
<div class="bg-gradient-to-br from-blue-50 to-indigo-100 p-4 rounded-md">
<pre class="text-gray-700 text-lg whitespace-pre-line">${data.workout}</pre>
</div>
</div>
`;
})
.catch(error => {
console.error('Error:', error);
const workoutContainer = document.getElementById('workout-container');
workoutContainer.innerHTML = `
<div id="error-message" class="p-6 bg-red-100 border border-red-400 text-red-700 rounded-lg">
<h2 class="font-bold text-xl mb-2">Error</h2>
<p>${error.message}</p>
</div>
`;
})
.finally(() => {
// Re-enable the button and hide loading indicator
generateBtn.disabled = false;
generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
loadingIndicator.classList.add('hidden');
});
});
</script>
</body>
</html>

0 comments on commit c85a5a1

Please sign in to comment.