-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit c85a5a1
Showing
2 changed files
with
392 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"> | ||
× | ||
</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> |