File size: 10,418 Bytes
51991fb
 
 
 
35f2f2e
51991fb
 
 
35f2f2e
51991fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35f2f2e
21d6b4e
787ee06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21d6b4e
35f2f2e
51991fb
787ee06
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51991fb
 
 
ecb2ffb
 
 
 
35f2f2e
ecb2ffb
 
 
51991fb
35f2f2e
51991fb
35f2f2e
03dc139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51991fb
ecb2ffb
35f2f2e
ecb2ffb
35f2f2e
ecb2ffb
51991fb
 
35f2f2e
51991fb
35f2f2e
 
 
 
 
 
 
787ee06
35f2f2e
 
51991fb
 
35f2f2e
51991fb
35f2f2e
 
 
 
 
 
 
 
 
787ee06
 
 
 
 
 
 
 
35f2f2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51991fb
 
35f2f2e
51991fb
 
 
 
 
 
 
 
 
 
21d6b4e
ecb2ffb
 
35f2f2e
 
ecb2ffb
 
 
35f2f2e
51991fb
 
35f2f2e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import os
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from stravalib.client import Client
import google.generativeai as genai
from dotenv import load_dotenv
from datetime import datetime, timedelta

# Load environment variables
load_dotenv()

# API Keys and Secrets
STRAVA_CLIENT_ID = os.getenv('STRAVA_CLIENT_ID')
STRAVA_CLIENT_SECRET = os.getenv('STRAVA_CLIENT_SECRET')
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')

# Strava API setup
client = Client()

# Gemini API setup
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel('gemini-pro')

# Streamlit app
st.set_page_config(page_title="Strava Run Analysis", layout="wide")

# Custom CSS for better UI
st.markdown("""
<style>
    .stApp {
        background-color: #f0f2f6;
    }
    .stButton>button {
        background-color: #fc4c02;
        color: white;
        font-weight: bold;
        border-radius: 5px;
        border: none;
        padding: 0.5rem 1rem;
    }
    .stButton>button:hover {
        background-color: #e34402;
    }
    .stSelectbox {
        color: #333333;
    }
    .stPlotlyChart {
        background-color: white;
        border-radius: 10px;
        box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        padding: 1rem;
        margin-bottom: 2rem;
    }
    .stat-card {
        background-color: white;
        border-radius: 10px;
        box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        padding: 1.5rem;
        text-align: center;
    }
    .stat-card h3 {
        color: #fc4c02;
        font-size: 2.5rem;
        margin-bottom: 0.5rem;
    }
    .stat-card p {
        color: #666;
        font-size: 1rem;
        margin: 0;
    }
    .section-header {
        color: #333;
        font-size: 1.8rem;
        font-weight: bold;
        margin-top: 2rem;
        margin-bottom: 1rem;
    }
</style>
""", unsafe_allow_html=True)

st.title("πŸƒβ€β™‚οΈ Strava Run Analysis")

# Strava manual authentication
if 'access_token' not in st.session_state:
    st.write("To use this app, you need to authorize it with Strava. Follow these steps:")
    st.write("1. Click the button below to go to Strava's authorization page:")
    auth_url = f"https://www.strava.com/oauth/authorize?client_id={STRAVA_CLIENT_ID}&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=read_all,profile:read_all,activity:read_all"
    st.markdown(f"<a href='{auth_url}' target='_blank'><button style='background-color: #fc4c02; color: white; padding: 0.5rem 1rem; border: none; border-radius: 5px; cursor: pointer;'>Authorize Strava</button></a>", unsafe_allow_html=True)
    st.write("2. Log in to Strava if needed and click 'Authorize'")
    st.write("3. After authorizing, you'll be redirected to a page that may show an error. This is expected!")
    st.write("4. Copy the 'code' parameter from the URL of that page.")
    st.write("5. Paste that code below:")
    
    auth_code = st.text_input("Paste the authorization code here:")
    if auth_code:
        try:
            token_response = client.exchange_code_for_token(
                client_id=STRAVA_CLIENT_ID,
                client_secret=STRAVA_CLIENT_SECRET,
                code=auth_code
            )
            st.session_state.access_token = token_response['access_token']
            st.success("Authorization successful! Refreshing the app...")
            st.rerun()
        except Exception as e:
            st.error(f"An error occurred: {str(e)}. Please try authorizing again.")

if 'access_token' in st.session_state:
    client.access_token = st.session_state.access_token
    try:
        athlete = client.get_athlete()
        st.write(f"Welcome, {athlete.firstname} {athlete.lastname}! πŸ‘‹")
    except Exception as e:
        st.error(f"Error fetching athlete data: {str(e)}. Please try reauthorizing.")
        if st.button("Reauthorize"):
            del st.session_state.access_token
            st.rerun()

    # Fetch running activities
    @st.cache_data(ttl=3600)
    def fetch_run_activities():
        activities = list(client.get_activities(limit=200))
        runs = []
        for activity in activities:
            if activity.type == 'Run':
                run_data = {
                    'name': activity.name,
                    'distance': float(activity.distance.num) / 1000 if activity.distance else None,  # Convert to km
                    'moving_time': None,
                    'total_elevation_gain': float(activity.total_elevation_gain) if activity.total_elevation_gain else None,
                    'average_speed': float(activity.average_speed) * 3.6 if activity.average_speed else None,  # Convert to km/h
                    'average_heartrate': float(activity.average_heartrate) if activity.average_heartrate else None,
                    'start_date': activity.start_date.replace(tzinfo=None) if activity.start_date else None
                }
                
                # Safely handle moving_time
                try:
                    if activity.moving_time:
                        run_data['moving_time'] = activity.moving_time.total_seconds() / 60  # Convert to minutes
                except AttributeError:
                    pass  # If moving_time is not accessible, leave it as None
                
                runs.append(run_data)
        
        df = pd.DataFrame(runs)
        
        # Calculate pace only if both moving_time and distance are available
        df['pace'] = df.apply(lambda row: row['moving_time'] / row['distance'] if row['moving_time'] and row['distance'] else None, axis=1)
        
        return df

    try:
        df = fetch_run_activities()
    except Exception as e:
        st.error(f"Error fetching activities: {str(e)}. Please try again later.")
        st.stop()

    # Basic stats
    st.markdown("<h2 class='section-header'>πŸ“Š Run Statistics</h2>", unsafe_allow_html=True)
    
    col1, col2, col3, col4 = st.columns(4)
    with col1:
        st.markdown("<div class='stat-card'><h3>{:.0f}</h3><p>Total Runs</p></div>".format(len(df)), unsafe_allow_html=True)
    with col2:
        st.markdown("<div class='stat-card'><h3>{:.0f} km</h3><p>Total Distance</p></div>".format(df['distance'].sum()), unsafe_allow_html=True)
    with col3:
        total_time = df['moving_time'].sum()
        st.markdown("<div class='stat-card'><h3>{:.0f}h {:.0f}m</h3><p>Total Time</p></div>".format(total_time // 60, total_time % 60), unsafe_allow_html=True)
    with col4:
        st.markdown("<div class='stat-card'><h3>{:.0f} m</h3><p>Total Elevation Gain</p></div>".format(df['total_elevation_gain'].sum()), unsafe_allow_html=True)

    # Visualizations
    st.markdown("<h2 class='section-header'>πŸ“ˆ Run Analysis</h2>", unsafe_allow_html=True)
    
    # Weekly distance
    weekly_distance = df.resample('W', on='start_date')['distance'].sum().reset_index()
    fig_weekly = px.bar(weekly_distance, x='start_date', y='distance', 
                        title="Weekly Running Distance",
                        labels={'distance': 'Distance (km)', 'start_date': 'Week'})
    fig_weekly.update_layout(xaxis_title="Week", yaxis_title="Distance (km)")
    st.plotly_chart(fig_weekly, use_container_width=True)

    # Pace improvement
    df_sorted = df.sort_values('start_date')
    fig_pace = px.scatter(df_sorted, x='start_date', y='pace', 
                          title="Running Pace Over Time",
                          labels={'pace': 'Pace (min/km)', 'start_date': 'Date'})
    fig_pace.add_trace(go.Scatter(x=df_sorted['start_date'], y=df_sorted['pace'].rolling(window=10).mean(),
                                  mode='lines', name='10-run moving average'))
    fig_pace.update_layout(yaxis_title="Pace (min/km)")
    st.plotly_chart(fig_pace, use_container_width=True)

    # Heart rate improvement (if data available)
    if df['average_heartrate'].notna().any():
        df_hr = df_sorted[df_sorted['average_heartrate'].notna()]
        fig_hr = px.scatter(df_hr, x='start_date', y='average_heartrate', 
                            title="Average Heart Rate Over Time",
                            labels={'average_heartrate': 'Average Heart Rate (bpm)', 'start_date': 'Date'})
        fig_hr.add_trace(go.Scatter(x=df_hr['start_date'], y=df_hr['average_heartrate'].rolling(window=10).mean(),
                                    mode='lines', name='10-run moving average'))
        fig_hr.update_layout(yaxis_title="Average Heart Rate (bpm)")
        st.plotly_chart(fig_hr, use_container_width=True)
    else:
        st.info("No heart rate data available for analysis.")

    # Distance vs. Elevation gain
    fig_elev = px.scatter(df, x='distance', y='total_elevation_gain',
                          title="Distance vs. Elevation Gain",
                          labels={'distance': 'Distance (km)', 'total_elevation_gain': 'Elevation Gain (m)'})
    fig_elev.update_layout(xaxis_title="Distance (km)", yaxis_title="Elevation Gain (m)")
    st.plotly_chart(fig_elev, use_container_width=True)

    # Training Plan Generation
    st.markdown("<h2 class='section-header'>πŸ‹οΈβ€β™€οΈ Generate Training Plan</h2>", unsafe_allow_html=True)
    
    col1, col2, col3 = st.columns(3)
    with col1:
        level = st.selectbox("Select your level:", ["Beginner", "Intermediate", "Advanced"])
    with col2:
        race_distance = st.selectbox("Select race distance:", ["5K", "10K", "Half Marathon", "Marathon"])
    with col3:
        plan_duration = st.selectbox("Select plan duration:", ["8 weeks", "10 weeks", "12 weeks"])

    if st.button("Generate Training Plan"):
        with st.spinner("Generating your personalized training plan..."):
            try:
                prompt = f"""Create a {plan_duration} training plan for a {level} runner preparing for a {race_distance} race. 
                Include weekly mileage and key workouts. Format the plan week by week, with each week on a new line. 
                Consider the runner's current weekly mileage of {weekly_distance['distance'].iloc[-1]:.1f} km."""
                response = model.generate_content(prompt)
                st.markdown(response.text)
            except Exception as e:
                st.error(f"Error generating training plan: {str(e)}. Please try again later.")

else:
    st.info("Please complete the authorization process to view your Strava running data and analytics.")