Spaces:
Sleeping
Sleeping
Kosala Nayanajith Deshapriya commited on
Commit Β·
d4ea2d4
1
Parent(s): 4dc7a21
update
Browse files- dashboard.py +37 -49
- youtube_auth.py +22 -15
dashboard.py
CHANGED
|
@@ -6,9 +6,9 @@ Full OAuth login + integrated pipeline (no subprocess)
|
|
| 6 |
import streamlit as st
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
-
import tempfile
|
| 10 |
import pandas as pd
|
| 11 |
import plotly.graph_objects as go
|
|
|
|
| 12 |
|
| 13 |
# ββ Page Config ββ
|
| 14 |
st.set_page_config(
|
|
@@ -66,16 +66,19 @@ st.markdown("""
|
|
| 66 |
</style>
|
| 67 |
""", unsafe_allow_html=True)
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
# ββ Helpers ββ
|
| 71 |
-
|
| 72 |
def get_spot_quality(retention):
|
| 73 |
if retention >= 50:
|
| 74 |
-
return "π’", "Great Spot",
|
| 75 |
elif retention >= 30:
|
| 76 |
-
return "π‘", "Decent Spot",
|
| 77 |
else:
|
| 78 |
-
return "π΄", "Weak Spot",
|
| 79 |
|
| 80 |
def get_type_label(t):
|
| 81 |
return {
|
|
@@ -107,40 +110,36 @@ def load_json(path):
|
|
| 107 |
with open(path) as f:
|
| 108 |
return json.load(f)
|
| 109 |
|
| 110 |
-
|
| 111 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 112 |
# HEADER
|
| 113 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
st.markdown('<div class="big-title">π― Ad Placement Recommender</div>', unsafe_allow_html=True)
|
| 115 |
st.markdown('<div class="subtitle">Find the perfect moments in your video to place ads β so viewers stay happy and you earn more.</div>', unsafe_allow_html=True)
|
| 116 |
|
| 117 |
-
|
| 118 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 119 |
# AUTH GATE
|
| 120 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
-
from youtube_auth import get_credentials, show_login_button, logout
|
| 122 |
-
|
| 123 |
creds = get_credentials()
|
| 124 |
|
| 125 |
if creds is None:
|
| 126 |
st.markdown("---")
|
| 127 |
-
st.markdown("""
|
| 128 |
-
<div class="login-card">
|
| 129 |
-
<div style="font-size:3rem;">π</div>
|
| 130 |
-
<div style="font-size:1.3rem; font-weight:700; color:#fff; margin:1rem 0;">
|
| 131 |
-
Connect Your YouTube Account
|
| 132 |
-
</div>
|
| 133 |
-
<div style="color:#9ca3af; font-size:0.9rem; margin-bottom:1.5rem;">
|
| 134 |
-
We need read-only access to your YouTube Analytics<br>
|
| 135 |
-
to see where viewers drop off in your videos.<br><br>
|
| 136 |
-
<b style="color:#6ee7b7;">We never post, modify or delete anything.</b>
|
| 137 |
-
</div>
|
| 138 |
-
</div>
|
| 139 |
-
""", unsafe_allow_html=True)
|
| 140 |
-
|
| 141 |
-
auth_url = show_login_button()
|
| 142 |
col1, col2, col3 = st.columns([1, 2, 1])
|
| 143 |
with col2:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
st.markdown(f"""
|
| 145 |
<a href="{auth_url}" target="_self" style="text-decoration:none;">
|
| 146 |
<div style="
|
|
@@ -155,7 +154,7 @@ if creds is None:
|
|
| 155 |
""", unsafe_allow_html=True)
|
| 156 |
st.stop()
|
| 157 |
|
| 158 |
-
# ββ
|
| 159 |
with st.sidebar:
|
| 160 |
st.markdown("### π€ Account")
|
| 161 |
st.success("β
YouTube Connected")
|
|
@@ -171,7 +170,6 @@ with st.sidebar:
|
|
| 171 |
4. π See exactly where to place ads
|
| 172 |
""")
|
| 173 |
|
| 174 |
-
|
| 175 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
# INPUT FORM
|
| 177 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -201,13 +199,11 @@ if analyze_btn:
|
|
| 201 |
if not video_id:
|
| 202 |
st.error("β Could not extract video ID. Please check your YouTube URL.")
|
| 203 |
else:
|
| 204 |
-
# Save uploaded video to temp file
|
| 205 |
os.makedirs("test_video", exist_ok=True)
|
| 206 |
save_path = os.path.join("test_video", uploaded_file.name)
|
| 207 |
with open(save_path, "wb") as f:
|
| 208 |
f.write(uploaded_file.getbuffer())
|
| 209 |
|
| 210 |
-
# Run pipeline with live progress
|
| 211 |
from pipeline import run_full_pipeline
|
| 212 |
|
| 213 |
progress_box = st.empty()
|
|
@@ -231,7 +227,6 @@ if analyze_btn:
|
|
| 231 |
|
| 232 |
st.markdown("---")
|
| 233 |
|
| 234 |
-
|
| 235 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 236 |
# LOAD RESULTS
|
| 237 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -249,18 +244,16 @@ total_placed = data.get("total_placements_recommended", 0)
|
|
| 249 |
cands_list = candidates.get("ranked_placements", []) if candidates else []
|
| 250 |
top_ret = max((c["retention_at_t"] for c in cands_list), default=0)
|
| 251 |
|
| 252 |
-
|
| 253 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 254 |
# METRICS
|
| 255 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
m1, m2, m3, m4 = st.columns(4)
|
| 257 |
-
|
| 258 |
-
("π¬ Video Duration",
|
| 259 |
-
("π Best Ad Spots Found",
|
| 260 |
-
("π¬ Moments Analyzed",
|
| 261 |
-
("π Best Spot Retention",
|
| 262 |
-
]
|
| 263 |
-
for col, (label, value, color) in zip([m1, m2, m3, m4], metrics):
|
| 264 |
with col:
|
| 265 |
st.markdown(f"""
|
| 266 |
<div class="metric-box">
|
|
@@ -270,15 +263,14 @@ for col, (label, value, color) in zip([m1, m2, m3, m4], metrics):
|
|
| 270 |
|
| 271 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 272 |
|
| 273 |
-
|
| 274 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 275 |
# RETENTION CURVE
|
| 276 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 277 |
st.markdown('<div class="section-header">π Your Viewer Retention Curve</div>', unsafe_allow_html=True)
|
| 278 |
-
st.caption("This shows how many viewers are still watching at each moment.
|
| 279 |
|
| 280 |
if retention:
|
| 281 |
-
times = [p["time_seconds"]
|
| 282 |
values = [p["retention_percent"] for p in retention]
|
| 283 |
|
| 284 |
fig = go.Figure()
|
|
@@ -310,7 +302,6 @@ if retention:
|
|
| 310 |
|
| 311 |
st.markdown("---")
|
| 312 |
|
| 313 |
-
|
| 314 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 315 |
# RECOMMENDATIONS
|
| 316 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -369,7 +360,6 @@ else:
|
|
| 369 |
|
| 370 |
st.markdown("---")
|
| 371 |
|
| 372 |
-
|
| 373 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 374 |
# INSIGHTS FOR NEXT VIDEO
|
| 375 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -384,8 +374,8 @@ if cands_list:
|
|
| 384 |
|
| 385 |
i1, i2, i3 = st.columns(3)
|
| 386 |
for col, label, value, color, sub in [
|
| 387 |
-
(i1, "π Peak Retention Spot",
|
| 388 |
-
(i2, "π Biggest Drop-Off At",
|
| 389 |
(i3, "π Avg Retention at Breaks", f"{avg_ret:.0f}%", "#f59e0b", "across all detected moments"),
|
| 390 |
]:
|
| 391 |
with col:
|
|
@@ -403,18 +393,17 @@ if cands_list:
|
|
| 403 |
if best_ret >= 60:
|
| 404 |
tips.append("β
**Strong early retention** β your intro hooks viewers well. Keep doing what you did in the first 2 minutes.")
|
| 405 |
if worst_ret < 25:
|
| 406 |
-
tips.append(f"β οΈ **Viewers drop off near {drop_time}** β tighten that section or add a re-engagement hook
|
| 407 |
if avg_ret < 35:
|
| 408 |
-
tips.append("π **Overall retention is low** β try shorter videos or stronger pacing
|
| 409 |
if avg_ret >= 50:
|
| 410 |
-
tips.append("π **Great overall retention** β
|
| 411 |
if total_placed == 0:
|
| 412 |
tips.append("π΄ **No strong ad spots this time** β aim to keep 40%+ viewers watching past the 2-minute mark.")
|
| 413 |
|
| 414 |
for tip in tips:
|
| 415 |
st.markdown(f"- {tip}")
|
| 416 |
|
| 417 |
-
|
| 418 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 419 |
# ADVANCED TABLE
|
| 420 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -428,7 +417,6 @@ with st.expander("π¬ View All Analyzed Moments (Advanced)"):
|
|
| 428 |
df_show["ML Score"] = df_show["ML Score"].map(lambda x: f"{x:.4f}")
|
| 429 |
st.dataframe(df_show, use_container_width=True, hide_index=True)
|
| 430 |
|
| 431 |
-
|
| 432 |
# ββ Footer ββ
|
| 433 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 434 |
st.markdown(
|
|
|
|
| 6 |
import streamlit as st
|
| 7 |
import json
|
| 8 |
import os
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
import plotly.graph_objects as go
|
| 11 |
+
from youtube_auth import get_credentials, show_login_button, logout
|
| 12 |
|
| 13 |
# ββ Page Config ββ
|
| 14 |
st.set_page_config(
|
|
|
|
| 66 |
</style>
|
| 67 |
""", unsafe_allow_html=True)
|
| 68 |
|
| 69 |
+
# ββ Handle post-login rerun FIRST (before anything else) ββ
|
| 70 |
+
if "just_logged_in" in st.session_state:
|
| 71 |
+
del st.session_state["just_logged_in"]
|
| 72 |
+
st.rerun()
|
| 73 |
|
| 74 |
# ββ Helpers ββ
|
|
|
|
| 75 |
def get_spot_quality(retention):
|
| 76 |
if retention >= 50:
|
| 77 |
+
return "π’", "Great Spot", "placement-card", "badge-green"
|
| 78 |
elif retention >= 30:
|
| 79 |
+
return "π‘", "Decent Spot", "placement-card-warn", "badge-yellow"
|
| 80 |
else:
|
| 81 |
+
return "π΄", "Weak Spot", "placement-card-bad", "badge-red"
|
| 82 |
|
| 83 |
def get_type_label(t):
|
| 84 |
return {
|
|
|
|
| 110 |
with open(path) as f:
|
| 111 |
return json.load(f)
|
| 112 |
|
|
|
|
| 113 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
# HEADER
|
| 115 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 116 |
st.markdown('<div class="big-title">π― Ad Placement Recommender</div>', unsafe_allow_html=True)
|
| 117 |
st.markdown('<div class="subtitle">Find the perfect moments in your video to place ads β so viewers stay happy and you earn more.</div>', unsafe_allow_html=True)
|
| 118 |
|
|
|
|
| 119 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 120 |
# AUTH GATE
|
| 121 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 122 |
creds = get_credentials()
|
| 123 |
|
| 124 |
if creds is None:
|
| 125 |
st.markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
col1, col2, col3 = st.columns([1, 2, 1])
|
| 127 |
with col2:
|
| 128 |
+
st.markdown("""
|
| 129 |
+
<div class="login-card">
|
| 130 |
+
<div style="font-size:3rem;">π</div>
|
| 131 |
+
<div style="font-size:1.3rem;font-weight:700;color:#fff;margin:1rem 0;">
|
| 132 |
+
Connect Your YouTube Account
|
| 133 |
+
</div>
|
| 134 |
+
<div style="color:#9ca3af;font-size:0.9rem;margin-bottom:1.5rem;">
|
| 135 |
+
We need read-only access to your YouTube Analytics<br>
|
| 136 |
+
to see where viewers drop off in your videos.<br><br>
|
| 137 |
+
<b style="color:#6ee7b7;">We never post, modify or delete anything.</b>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
""", unsafe_allow_html=True)
|
| 141 |
+
|
| 142 |
+
auth_url = show_login_button()
|
| 143 |
st.markdown(f"""
|
| 144 |
<a href="{auth_url}" target="_self" style="text-decoration:none;">
|
| 145 |
<div style="
|
|
|
|
| 154 |
""", unsafe_allow_html=True)
|
| 155 |
st.stop()
|
| 156 |
|
| 157 |
+
# ββ Sidebar (logged in) ββ
|
| 158 |
with st.sidebar:
|
| 159 |
st.markdown("### π€ Account")
|
| 160 |
st.success("β
YouTube Connected")
|
|
|
|
| 170 |
4. π See exactly where to place ads
|
| 171 |
""")
|
| 172 |
|
|
|
|
| 173 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 174 |
# INPUT FORM
|
| 175 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 199 |
if not video_id:
|
| 200 |
st.error("β Could not extract video ID. Please check your YouTube URL.")
|
| 201 |
else:
|
|
|
|
| 202 |
os.makedirs("test_video", exist_ok=True)
|
| 203 |
save_path = os.path.join("test_video", uploaded_file.name)
|
| 204 |
with open(save_path, "wb") as f:
|
| 205 |
f.write(uploaded_file.getbuffer())
|
| 206 |
|
|
|
|
| 207 |
from pipeline import run_full_pipeline
|
| 208 |
|
| 209 |
progress_box = st.empty()
|
|
|
|
| 227 |
|
| 228 |
st.markdown("---")
|
| 229 |
|
|
|
|
| 230 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 231 |
# LOAD RESULTS
|
| 232 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 244 |
cands_list = candidates.get("ranked_placements", []) if candidates else []
|
| 245 |
top_ret = max((c["retention_at_t"] for c in cands_list), default=0)
|
| 246 |
|
|
|
|
| 247 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 248 |
# METRICS
|
| 249 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 250 |
m1, m2, m3, m4 = st.columns(4)
|
| 251 |
+
for col, label, value, color in [
|
| 252 |
+
(m1, "π¬ Video Duration", duration, "#ffffff"),
|
| 253 |
+
(m2, "π Best Ad Spots Found", total_placed, "#22c55e" if total_placed > 0 else "#ef4444"),
|
| 254 |
+
(m3, "π¬ Moments Analyzed", len(cands_list), "#ffffff"),
|
| 255 |
+
(m4, "π Best Spot Retention", f"{top_ret:.0f}%", "#22c55e" if top_ret >= 50 else "#f59e0b"),
|
| 256 |
+
]:
|
|
|
|
| 257 |
with col:
|
| 258 |
st.markdown(f"""
|
| 259 |
<div class="metric-box">
|
|
|
|
| 263 |
|
| 264 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 265 |
|
|
|
|
| 266 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 267 |
# RETENTION CURVE
|
| 268 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 269 |
st.markdown('<div class="section-header">π Your Viewer Retention Curve</div>', unsafe_allow_html=True)
|
| 270 |
+
st.caption("This shows how many viewers are still watching at each moment. Markers show recommended ad spots.")
|
| 271 |
|
| 272 |
if retention:
|
| 273 |
+
times = [p["time_seconds"] for p in retention]
|
| 274 |
values = [p["retention_percent"] for p in retention]
|
| 275 |
|
| 276 |
fig = go.Figure()
|
|
|
|
| 302 |
|
| 303 |
st.markdown("---")
|
| 304 |
|
|
|
|
| 305 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 306 |
# RECOMMENDATIONS
|
| 307 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 360 |
|
| 361 |
st.markdown("---")
|
| 362 |
|
|
|
|
| 363 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 364 |
# INSIGHTS FOR NEXT VIDEO
|
| 365 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 374 |
|
| 375 |
i1, i2, i3 = st.columns(3)
|
| 376 |
for col, label, value, color, sub in [
|
| 377 |
+
(i1, "π Peak Retention Spot", f"{best_ret:.0f}%", "#22c55e", "viewers at best moment"),
|
| 378 |
+
(i2, "π Biggest Drop-Off At", drop_time, "#ef4444", "most viewers left here"),
|
| 379 |
(i3, "π Avg Retention at Breaks", f"{avg_ret:.0f}%", "#f59e0b", "across all detected moments"),
|
| 380 |
]:
|
| 381 |
with col:
|
|
|
|
| 393 |
if best_ret >= 60:
|
| 394 |
tips.append("β
**Strong early retention** β your intro hooks viewers well. Keep doing what you did in the first 2 minutes.")
|
| 395 |
if worst_ret < 25:
|
| 396 |
+
tips.append(f"β οΈ **Viewers drop off near {drop_time}** β tighten that section or add a re-engagement hook.")
|
| 397 |
if avg_ret < 35:
|
| 398 |
+
tips.append("π **Overall retention is low** β try shorter videos or stronger pacing.")
|
| 399 |
if avg_ret >= 50:
|
| 400 |
+
tips.append("π **Great overall retention** β you can confidently place ads and expect good visibility.")
|
| 401 |
if total_placed == 0:
|
| 402 |
tips.append("π΄ **No strong ad spots this time** β aim to keep 40%+ viewers watching past the 2-minute mark.")
|
| 403 |
|
| 404 |
for tip in tips:
|
| 405 |
st.markdown(f"- {tip}")
|
| 406 |
|
|
|
|
| 407 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 408 |
# ADVANCED TABLE
|
| 409 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 417 |
df_show["ML Score"] = df_show["ML Score"].map(lambda x: f"{x:.4f}")
|
| 418 |
st.dataframe(df_show, use_container_width=True, hide_index=True)
|
| 419 |
|
|
|
|
| 420 |
# ββ Footer ββ
|
| 421 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 422 |
st.markdown(
|
youtube_auth.py
CHANGED
|
@@ -39,28 +39,36 @@ def build_flow():
|
|
| 39 |
return flow
|
| 40 |
|
| 41 |
def get_credentials():
|
|
|
|
| 42 |
if "google_creds" in st.session_state:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
creds.
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
|
|
|
| 54 |
if "code" in params:
|
| 55 |
try:
|
| 56 |
flow = build_flow()
|
| 57 |
flow.fetch_token(code=params["code"])
|
| 58 |
creds = flow.credentials
|
|
|
|
| 59 |
st.session_state["google_creds"] = json.loads(creds.to_json())
|
|
|
|
| 60 |
st.query_params.clear()
|
| 61 |
return creds
|
| 62 |
except Exception as e:
|
| 63 |
-
st.error(f"Login failed: {e}")
|
|
|
|
| 64 |
return None
|
| 65 |
|
| 66 |
return None
|
|
@@ -74,7 +82,6 @@ def show_login_button():
|
|
| 74 |
return auth_url
|
| 75 |
|
| 76 |
def logout():
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
del st.session_state["user_info"]
|
|
|
|
| 39 |
return flow
|
| 40 |
|
| 41 |
def get_credentials():
|
| 42 |
+
# Already authenticated this session
|
| 43 |
if "google_creds" in st.session_state:
|
| 44 |
+
try:
|
| 45 |
+
creds = Credentials.from_authorized_user_info(
|
| 46 |
+
st.session_state["google_creds"], SCOPES
|
| 47 |
+
)
|
| 48 |
+
if creds.valid:
|
| 49 |
+
return creds
|
| 50 |
+
if creds.expired and creds.refresh_token:
|
| 51 |
+
creds.refresh(Request())
|
| 52 |
+
st.session_state["google_creds"] = json.loads(creds.to_json())
|
| 53 |
+
return creds
|
| 54 |
+
except Exception:
|
| 55 |
+
del st.session_state["google_creds"]
|
| 56 |
|
| 57 |
+
# Check for OAuth callback code in URL
|
| 58 |
+
params = dict(st.query_params)
|
| 59 |
if "code" in params:
|
| 60 |
try:
|
| 61 |
flow = build_flow()
|
| 62 |
flow.fetch_token(code=params["code"])
|
| 63 |
creds = flow.credentials
|
| 64 |
+
# Save BEFORE clearing params
|
| 65 |
st.session_state["google_creds"] = json.loads(creds.to_json())
|
| 66 |
+
st.session_state["just_logged_in"] = True
|
| 67 |
st.query_params.clear()
|
| 68 |
return creds
|
| 69 |
except Exception as e:
|
| 70 |
+
st.error(f"Login failed: {e}. Please try again.")
|
| 71 |
+
st.query_params.clear()
|
| 72 |
return None
|
| 73 |
|
| 74 |
return None
|
|
|
|
| 82 |
return auth_url
|
| 83 |
|
| 84 |
def logout():
|
| 85 |
+
for key in ["google_creds", "just_logged_in", "user_info"]:
|
| 86 |
+
if key in st.session_state:
|
| 87 |
+
del st.session_state[key]
|
|
|