|
|
import streamlit as st |
|
|
import json |
|
|
from datetime import datetime |
|
|
import time |
|
|
import requests |
|
|
import boto3 |
|
|
import uuid |
|
|
import os |
|
|
|
|
|
|
|
|
API_ENDPOINT = os.getenv("API_ENDPOINT", "https://68p3txfrz2.execute-api.ap-south-1.amazonaws.com/dev/process") |
|
|
|
|
|
|
|
|
DYNAMODB_REGION = os.getenv("DYNAMODB_REGION", "ap-south-1") |
|
|
SESSION_TABLE = os.getenv("SESSION_TABLE", "SessionTracking") |
|
|
|
|
|
|
|
|
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") |
|
|
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") |
|
|
AWS_DEFAULT_REGION = os.getenv("AWS_DEFAULT_REGION", "ap-south-1") |
|
|
|
|
|
|
|
|
DEFAULT_USER_ID = int(os.getenv("DEFAULT_USER_ID", "30")) |
|
|
DEFAULT_PERSONALIZATION_ID = int(os.getenv("DEFAULT_PERSONALIZATION_ID", "100")) |
|
|
DEFAULT_COURSE_ID = int(os.getenv("DEFAULT_COURSE_ID", "47")) |
|
|
DEFAULT_CHAPTER_ID = int(os.getenv("DEFAULT_CHAPTER_ID", "647")) |
|
|
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30")) |
|
|
|
|
|
|
|
|
VALID_USERNAME = os.getenv("APP_USERNAME") |
|
|
VALID_PASSWORD = os.getenv("APP_PASSWORD") |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Base Course Personalization", |
|
|
layout="wide", |
|
|
initial_sidebar_state="collapsed", |
|
|
page_icon="π", |
|
|
menu_items={ |
|
|
'Get Help': None, |
|
|
'Report a bug': None, |
|
|
'About': None |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Dark theme colors */ |
|
|
:root { |
|
|
--background-color: #1E1E1E; |
|
|
--card-background: #2D2D2D; |
|
|
--text-color: #E0E0E0; |
|
|
--accent-color: #4F97FF; |
|
|
--border-color: #444444; |
|
|
--header-color: #4F97FF; |
|
|
--subheader-color: #FFFFFF; |
|
|
--success-color: #10B981; |
|
|
--warning-color: #F59E0B; |
|
|
--error-color: #EF4444; |
|
|
} |
|
|
|
|
|
/* Main container styling */ |
|
|
.stApp { |
|
|
background-color: var(--background-color); |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
/* Headers */ |
|
|
.main-header { |
|
|
font-size: 2.5rem; |
|
|
font-weight: 600; |
|
|
color: var(--header-color); |
|
|
margin-bottom: 1rem; |
|
|
text-align: center; |
|
|
text-shadow: 0 2px 4px rgba(79, 151, 255, 0.3); |
|
|
} |
|
|
|
|
|
.section-header { |
|
|
font-size: 1.5rem; |
|
|
font-weight: 500; |
|
|
color: var(--subheader-color); |
|
|
margin-top: 2rem; |
|
|
margin-bottom: 1rem; |
|
|
padding-bottom: 0.5rem; |
|
|
border-bottom: 2px solid var(--border-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
/* Form container */ |
|
|
div[data-testid="stForm"] { |
|
|
background-color: var(--card-background); |
|
|
padding: 2rem; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.4); |
|
|
border: 1px solid var(--border-color); |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
/* Button styling */ |
|
|
.stButton>button { |
|
|
background: linear-gradient(135deg, var(--accent-color) 0%, #3B82F6 100%); |
|
|
color: white; |
|
|
border-radius: 8px; |
|
|
padding: 0.75rem 2rem; |
|
|
font-weight: 600; |
|
|
border: none; |
|
|
transition: all 0.3s ease; |
|
|
box-shadow: 0 4px 15px rgba(79, 151, 255, 0.3); |
|
|
} |
|
|
|
|
|
.stButton>button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 20px rgba(79, 151, 255, 0.4); |
|
|
} |
|
|
|
|
|
/* Input fields styling */ |
|
|
div[data-baseweb="select"] > div { |
|
|
background-color: var(--background-color); |
|
|
border: 2px solid var(--border-color); |
|
|
border-radius: 8px; |
|
|
transition: border-color 0.3s ease; |
|
|
} |
|
|
|
|
|
div[data-baseweb="select"] > div:focus-within { |
|
|
border-color: var(--accent-color); |
|
|
box-shadow: 0 0 0 3px rgba(79, 151, 255, 0.1); |
|
|
} |
|
|
|
|
|
.stTextInput > div > div > input, |
|
|
.stNumberInput > div > div > input, |
|
|
.stTextArea > div > div > textarea { |
|
|
background-color: var(--background-color); |
|
|
color: var(--text-color); |
|
|
border: 2px solid var(--border-color); |
|
|
border-radius: 8px; |
|
|
padding: 0.75rem; |
|
|
transition: border-color 0.3s ease; |
|
|
} |
|
|
|
|
|
.stTextInput > div > div > input:focus, |
|
|
.stNumberInput > div > div > input:focus, |
|
|
.stTextArea > div > div > textarea:focus { |
|
|
border-color: var(--accent-color); |
|
|
box-shadow: 0 0 0 3px rgba(79, 151, 255, 0.1); |
|
|
} |
|
|
|
|
|
/* Topic container styling */ |
|
|
.topic-container { |
|
|
background-color: var(--card-background); |
|
|
padding: 1.5rem; |
|
|
border-radius: 10px; |
|
|
margin-bottom: 1rem; |
|
|
border: 1px solid var(--border-color); |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.topic-header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 0.5rem; |
|
|
margin-bottom: 1rem; |
|
|
font-weight: 600; |
|
|
color: var(--accent-color); |
|
|
} |
|
|
|
|
|
/* Radio buttons */ |
|
|
.stRadio > div { |
|
|
background-color: transparent; |
|
|
gap: 1rem; |
|
|
} |
|
|
|
|
|
.stRadio > div > label > div { |
|
|
color: var(--text-color) !important; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
/* Labels */ |
|
|
.stSelectbox > label, |
|
|
.stTextInput > label, |
|
|
.stNumberInput > label, |
|
|
.stTextArea > label, |
|
|
.stRadio > label { |
|
|
color: var(--text-color) !important; |
|
|
font-weight: 500; |
|
|
margin-bottom: 0.5rem; |
|
|
} |
|
|
|
|
|
/* Toggle switch */ |
|
|
.stToggle > label { |
|
|
color: var(--text-color) !important; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
/* Spacing */ |
|
|
div.block-container { |
|
|
padding-top: 2rem; |
|
|
max-width: 1200px; |
|
|
} |
|
|
|
|
|
hr { |
|
|
margin: 2rem 0; |
|
|
border: none; |
|
|
border-top: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
/* API Response Container */ |
|
|
.api-response { |
|
|
background-color: #1A1A1A; |
|
|
border-radius: 10px; |
|
|
padding: 1.5rem; |
|
|
border-left: 4px solid var(--accent-color); |
|
|
margin: 1rem 0; |
|
|
} |
|
|
|
|
|
.session-info { |
|
|
background-color: var(--card-background); |
|
|
padding: 1.5rem; |
|
|
border-radius: 10px; |
|
|
margin: 1rem 0; |
|
|
border: 1px solid var(--border-color); |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
/* Action buttons container */ |
|
|
.action-buttons { |
|
|
display: flex; |
|
|
gap: 1rem; |
|
|
margin: 1rem 0; |
|
|
justify-content: flex-start; |
|
|
} |
|
|
|
|
|
/* Small action buttons */ |
|
|
.small-button { |
|
|
padding: 0.5rem 1rem; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
/* Success/Error styling */ |
|
|
.stSuccess, .stError, .stWarning, .stInfo { |
|
|
border-radius: 8px; |
|
|
padding: 1rem; |
|
|
margin: 1rem 0; |
|
|
} |
|
|
|
|
|
/* Login container */ |
|
|
.login-container { |
|
|
background-color: var(--card-background); |
|
|
padding: 3rem; |
|
|
border-radius: 15px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5); |
|
|
border: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
/* Admin Dashboard specific styles */ |
|
|
.admin-card { |
|
|
background-color: var(--card-background); |
|
|
padding: 2rem; |
|
|
border-radius: 12px; |
|
|
border: 1px solid var(--border-color); |
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2); |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
|
|
|
.status-badge { |
|
|
padding: 0.3rem 0.8rem; |
|
|
border-radius: 20px; |
|
|
font-size: 0.8rem; |
|
|
font-weight: 600; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.5px; |
|
|
} |
|
|
|
|
|
.status-completed { background-color: #10B981; color: white; } |
|
|
.status-started { background-color: #F59E0B; color: white; } |
|
|
.status-failed { background-color: #EF4444; color: white; } |
|
|
|
|
|
.download-link { |
|
|
background-color: var(--accent-color); |
|
|
color: white; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 6px; |
|
|
text-decoration: none; |
|
|
font-size: 0.9rem; |
|
|
font-weight: 500; |
|
|
transition: background-color 0.3s ease; |
|
|
} |
|
|
|
|
|
.download-link:hover { |
|
|
background-color: #3B82F6; |
|
|
color: white; |
|
|
text-decoration: none; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if 'topics_list' not in st.session_state: |
|
|
st.session_state.topics_list = [{"topic_title": "What is Flask", "chapter_title": "Introduction to Flask"}] |
|
|
if 'session_ids' not in st.session_state: |
|
|
st.session_state.session_ids = [] |
|
|
if 'authenticated' not in st.session_state: |
|
|
st.session_state.authenticated = False |
|
|
if 'current_page' not in st.session_state: |
|
|
st.session_state.current_page = "Course Generation" |
|
|
if 'manual_session_id' not in st.session_state: |
|
|
st.session_state.manual_session_id = "" |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def get_dynamodb_client(): |
|
|
"""Initialize DynamoDB client with credentials from environment""" |
|
|
try: |
|
|
if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: |
|
|
return boto3.client( |
|
|
'dynamodb', |
|
|
region_name=DYNAMODB_REGION, |
|
|
aws_access_key_id=AWS_ACCESS_KEY_ID, |
|
|
aws_secret_access_key=AWS_SECRET_ACCESS_KEY |
|
|
) |
|
|
else: |
|
|
|
|
|
return boto3.client('dynamodb', region_name=DYNAMODB_REGION) |
|
|
except Exception as e: |
|
|
st.error(f"Failed to initialize DynamoDB client: {e}") |
|
|
return None |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def get_s3_client(): |
|
|
"""Initialize S3 client with credentials from environment""" |
|
|
try: |
|
|
if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: |
|
|
return boto3.client( |
|
|
's3', |
|
|
region_name=AWS_DEFAULT_REGION, |
|
|
aws_access_key_id=AWS_ACCESS_KEY_ID, |
|
|
aws_secret_access_key=AWS_SECRET_ACCESS_KEY |
|
|
) |
|
|
else: |
|
|
return boto3.client('s3', region_name=AWS_DEFAULT_REGION) |
|
|
except Exception as e: |
|
|
st.error(f"Failed to initialize S3 client: {e}") |
|
|
return None |
|
|
|
|
|
def get_session_data(session_id): |
|
|
"""Get session data from DynamoDB""" |
|
|
dynamodb = get_dynamodb_client() |
|
|
if not dynamodb: |
|
|
return None |
|
|
|
|
|
try: |
|
|
response = dynamodb.get_item( |
|
|
TableName=SESSION_TABLE, |
|
|
Key={'session_id': {'S': session_id}} |
|
|
) |
|
|
|
|
|
if 'Item' in response: |
|
|
item = response['Item'] |
|
|
return { |
|
|
'session_id': item.get('session_id', {}).get('S', ''), |
|
|
'status': item.get('status', {}).get('S', ''), |
|
|
'node': item.get('node', {}).get('S', ''), |
|
|
'course_id': item.get('course_id', {}).get('N', '0'), |
|
|
'topic_id': item.get('topic_id', {}).get('N', '0'), |
|
|
'topic_title': item.get('topic_title', {}).get('S', ''), |
|
|
'video_url': item.get('video_url', {}).get('S', ''), |
|
|
'created_at': item.get('created_at', {}).get('S', ''), |
|
|
'updated_at': item.get('updated_at', {}).get('S', '') |
|
|
} |
|
|
except Exception as e: |
|
|
st.error(f"Error fetching session data: {e}") |
|
|
|
|
|
return None |
|
|
|
|
|
def update_session_status(session_id, status, node=None, video_url=None): |
|
|
"""Update session status in DynamoDB""" |
|
|
dynamodb = get_dynamodb_client() |
|
|
if not dynamodb: |
|
|
return False |
|
|
|
|
|
try: |
|
|
now = datetime.utcnow().isoformat() + "Z" |
|
|
|
|
|
update_expr = "SET updated_at = :u, #st = :s" |
|
|
expr_attr_names = {"#st": "status", "#ut": "updated_at"} |
|
|
expr_attr_values = {":u": {"S": now}, ":s": {"S": status}} |
|
|
|
|
|
if node: |
|
|
update_expr += ", #nd = :n" |
|
|
expr_attr_names["#nd"] = "node" |
|
|
expr_attr_values[":n"] = {"S": node} |
|
|
|
|
|
if video_url: |
|
|
update_expr += ", video_url = :v" |
|
|
expr_attr_values[":v"] = {"S": video_url} |
|
|
|
|
|
dynamodb.update_item( |
|
|
TableName=SESSION_TABLE, |
|
|
Key={'session_id': {'S': session_id}}, |
|
|
UpdateExpression=update_expr, |
|
|
ExpressionAttributeNames=expr_attr_names, |
|
|
ExpressionAttributeValues=expr_attr_values |
|
|
) |
|
|
return True |
|
|
except Exception as e: |
|
|
st.error(f"Error updating session: {e}") |
|
|
return False |
|
|
|
|
|
def generate_s3_presigned_url(s3_key, bucket='tech-learn-state', expiry=3600): |
|
|
"""Generate presigned URL for S3 object""" |
|
|
s3_client = get_s3_client() |
|
|
if not s3_client: |
|
|
return None |
|
|
|
|
|
try: |
|
|
url = s3_client.generate_presigned_url( |
|
|
'get_object', |
|
|
Params={'Bucket': bucket, 'Key': s3_key}, |
|
|
ExpiresIn=expiry |
|
|
) |
|
|
return url |
|
|
except Exception as e: |
|
|
st.error(f"Error generating presigned URL: {e}") |
|
|
return None |
|
|
|
|
|
def render_admin_dashboard(): |
|
|
"""Render the simplified admin dashboard""" |
|
|
st.markdown('<div class="section-header">ποΈ Admin Dashboard - Session Monitoring</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown('<div class="admin-card">', unsafe_allow_html=True) |
|
|
st.markdown("### π Session Lookup") |
|
|
|
|
|
col1, col2 = st.columns([3, 1]) |
|
|
with col1: |
|
|
session_id_input = st.text_input( |
|
|
"Enter Session ID", |
|
|
placeholder="e.g., sess_abc123 or 2025-01-15-12-30-45-abc123", |
|
|
help="Enter the session ID to monitor" |
|
|
) |
|
|
with col2: |
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
lookup_button = st.button("π Lookup", use_container_width=True) |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if lookup_button and session_id_input: |
|
|
session_data = get_session_data(session_id_input.strip()) |
|
|
|
|
|
if session_data: |
|
|
st.markdown('<div class="admin-card">', unsafe_allow_html=True) |
|
|
st.markdown(f"### π Session Details: `{session_id_input}`") |
|
|
|
|
|
|
|
|
status = session_data['status'] |
|
|
status_class = f"status-{status.lower()}" if status.lower() in ['completed', 'started', 'failed'] else "status-started" |
|
|
st.markdown(f'<span class="status-badge {status_class}">{status}</span>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.markdown(f""" |
|
|
**Node**: {session_data['node']} |
|
|
**Course ID**: {session_data['course_id']} |
|
|
**Topic ID**: {session_data['topic_id']} |
|
|
**Topic Title**: {session_data['topic_title']} |
|
|
""") |
|
|
with col2: |
|
|
st.markdown(f""" |
|
|
**Created**: {session_data['created_at'][:19] if session_data['created_at'] else 'N/A'} |
|
|
**Updated**: {session_data['updated_at'][:19] if session_data['updated_at'] else 'N/A'} |
|
|
**Status**: {session_data['status']} |
|
|
""") |
|
|
|
|
|
|
|
|
if session_data['video_url']: |
|
|
st.markdown("### π₯ Video Download") |
|
|
st.markdown(f'<a href="{session_data["video_url"]}" class="download-link" target="_blank">π₯ Download Video</a>', unsafe_allow_html=True) |
|
|
else: |
|
|
st.info("No video URL available for this session") |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown('<div class="admin-card">', unsafe_allow_html=True) |
|
|
st.markdown("### βοΈ Session Management") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
new_status = st.selectbox("Update Status", ["STARTED", "COMPLETED", "FAILED"], key="status_update") |
|
|
if st.button("π Update Status"): |
|
|
if update_session_status(session_id_input.strip(), new_status): |
|
|
st.success(f"Status updated to {new_status}") |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("Failed to update status") |
|
|
|
|
|
with col2: |
|
|
new_node = st.text_input("Update Node", placeholder="e.g., SlideCreation", key="node_update") |
|
|
if st.button("π Update Node"): |
|
|
if new_node.strip() and update_session_status(session_id_input.strip(), session_data['status'], new_node.strip()): |
|
|
st.success(f"Node updated to {new_node}") |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("Failed to update node") |
|
|
|
|
|
with col3: |
|
|
|
|
|
s3_key_input = st.text_input("S3 Key", placeholder="video_states/session_id/final_video/...", key="s3_key") |
|
|
if st.button("π Generate Video URL"): |
|
|
if s3_key_input.strip(): |
|
|
video_url = generate_s3_presigned_url(s3_key_input.strip()) |
|
|
if video_url: |
|
|
if update_session_status(session_id_input.strip(), session_data['status'], video_url=video_url): |
|
|
st.success("Video URL generated and updated!") |
|
|
st.code(video_url) |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("Failed to update video URL") |
|
|
else: |
|
|
st.error("Failed to generate presigned URL") |
|
|
else: |
|
|
st.error("Please enter S3 key") |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
else: |
|
|
st.error(f"β Session not found: `{session_id_input}`") |
|
|
|
|
|
|
|
|
if st.session_state.session_ids: |
|
|
st.markdown('<div class="admin-card">', unsafe_allow_html=True) |
|
|
st.markdown("### π Recent Sessions from Current App Session") |
|
|
|
|
|
for session_id in st.session_state.session_ids[-10:]: |
|
|
session_data = get_session_data(session_id) |
|
|
if session_data: |
|
|
with st.expander(f"π {session_id} - {session_data['status']}", expanded=False): |
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.write(f"**Node**: {session_data['node']}") |
|
|
st.write(f"**Topic**: {session_data['topic_title']}") |
|
|
st.write(f"**Status**: {session_data['status']}") |
|
|
with col2: |
|
|
st.write(f"**Updated**: {session_data['updated_at'][:19] if session_data['updated_at'] else 'N/A'}") |
|
|
if session_data['video_url']: |
|
|
st.markdown(f'<a href="{session_data["video_url"]}" class="download-link" target="_blank">π₯ Download</a>', unsafe_allow_html=True) |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if not st.session_state.authenticated: |
|
|
st.markdown('<h1 class="main-header">π Login to Course Management</h1>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
if not VALID_USERNAME or not VALID_PASSWORD: |
|
|
st.error("β Authentication credentials not configured. Please contact administrator.") |
|
|
st.stop() |
|
|
|
|
|
col1, col2, col3 = st.columns([1, 2, 1]) |
|
|
with col2: |
|
|
with st.container(): |
|
|
st.markdown('<div class="login-container">', unsafe_allow_html=True) |
|
|
|
|
|
with st.form("login_form"): |
|
|
st.markdown('<div class="section-header">π Authentication Required</div>', unsafe_allow_html=True) |
|
|
|
|
|
username = st.text_input("Username", placeholder="Enter username", key="login_username") |
|
|
password = st.text_input("Password", placeholder="Enter password", type="password", key="login_password") |
|
|
|
|
|
login_submitted = st.form_submit_button("π Login", use_container_width=True) |
|
|
|
|
|
if login_submitted: |
|
|
if VALID_USERNAME and VALID_PASSWORD and username == VALID_USERNAME and password == VALID_PASSWORD: |
|
|
st.session_state.authenticated = True |
|
|
st.success("Login successful! Redirecting...") |
|
|
time.sleep(1) |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("β Invalid username or password") |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown(""" |
|
|
<div style="text-align: center; color: #888888;"> |
|
|
<small>Please contact administrator for access credentials</small> |
|
|
</div> |
|
|
""", unsafe_allow_html=True) |
|
|
st.stop() |
|
|
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns([3, 2, 2, 1]) |
|
|
with col1: |
|
|
st.markdown('<h1 class="main-header">π Course Management System</h1>', unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
page = st.selectbox( |
|
|
"π Navigate to:", |
|
|
["Course Generation", "Admin Dashboard"], |
|
|
index=0 if st.session_state.current_page == "Course Generation" else 1, |
|
|
key="page_selector" |
|
|
) |
|
|
st.session_state.current_page = page |
|
|
|
|
|
with col4: |
|
|
if st.button("π Logout", key="logout_btn"): |
|
|
st.session_state.authenticated = False |
|
|
st.session_state.topics_list = [{"topic_title": "What is Flask", "chapter_title": "Introduction to Flask"}] |
|
|
st.session_state.session_ids = [] |
|
|
st.session_state.current_page = "Course Generation" |
|
|
st.session_state.manual_session_id = "" |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
if st.session_state.current_page == "Admin Dashboard": |
|
|
render_admin_dashboard() |
|
|
else: |
|
|
|
|
|
|
|
|
|
|
|
st.markdown('<div class="section-header">π― Session ID Configuration</div>', unsafe_allow_html=True) |
|
|
|
|
|
col1, col2 = st.columns([3, 2]) |
|
|
with col1: |
|
|
manual_session_id_input = st.text_input( |
|
|
"Manual Session ID (Optional)", |
|
|
value=st.session_state.manual_session_id, |
|
|
placeholder="Leave empty for auto-generated session ID (e.g., sess_abc12345)", |
|
|
help="Provide a custom session ID or leave empty to auto-generate" |
|
|
) |
|
|
st.session_state.manual_session_id = manual_session_id_input |
|
|
with col2: |
|
|
st.info("π‘ If provided, this session ID will be used instead of auto-generating one") |
|
|
|
|
|
|
|
|
st.markdown('<div class="section-header">π Topics Configuration</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
for i, topic in enumerate(st.session_state.topics_list): |
|
|
st.markdown(f'<div class="topic-container">', unsafe_allow_html=True) |
|
|
st.markdown(f'<div class="topic-header">π Topic {i+1}</div>', unsafe_allow_html=True) |
|
|
|
|
|
col1, col2, col3 = st.columns([3, 3, 1]) |
|
|
with col1: |
|
|
topic_title = st.text_input( |
|
|
"Topic Title", |
|
|
value=topic["topic_title"], |
|
|
key=f"topic_title_{i}", |
|
|
help="Enter the topic title" |
|
|
) |
|
|
st.session_state.topics_list[i]["topic_title"] = topic_title |
|
|
|
|
|
with col2: |
|
|
chapter_title = st.text_input( |
|
|
"Chapter Title", |
|
|
value=topic["chapter_title"], |
|
|
key=f"chapter_title_{i}", |
|
|
help="Enter the chapter title" |
|
|
) |
|
|
st.session_state.topics_list[i]["chapter_title"] = chapter_title |
|
|
|
|
|
with col3: |
|
|
if len(st.session_state.topics_list) > 1: |
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
if st.button("ποΈ", key=f"remove_{i}", help="Remove this topic"): |
|
|
st.session_state.topics_list.pop(i) |
|
|
st.rerun() |
|
|
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.markdown('<div class="action-buttons">', unsafe_allow_html=True) |
|
|
col1, col2, col3, col4 = st.columns([2, 2, 2, 4]) |
|
|
with col1: |
|
|
if st.button("β Add Topic", key="add_topic", help="Add a new topic"): |
|
|
st.session_state.topics_list.append({ |
|
|
"topic_title": f"Topic {len(st.session_state.topics_list) + 1}", |
|
|
"chapter_title": f"Chapter {len(st.session_state.topics_list) + 1}" |
|
|
}) |
|
|
st.rerun() |
|
|
with col2: |
|
|
if st.button("π Reset All", key="reset_topics", help="Reset to default topics"): |
|
|
st.session_state.topics_list = [{"topic_title": "What is Flask", "chapter_title": "Introduction to Flask"}] |
|
|
st.rerun() |
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.form("personalization_form", clear_on_submit=False): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.markdown('<div class="section-header">π£οΈ Language & Voice Settings</div>', unsafe_allow_html=True) |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
with col1: |
|
|
target_language = st.selectbox( |
|
|
"Target Language", |
|
|
["english", "hindi", "marathi", "kannada", "punjabi", "gujarati"], |
|
|
index=0, |
|
|
format_func=lambda x: x.capitalize(), |
|
|
help="Select the target language for content generation" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
tts_gender = st.selectbox( |
|
|
"Voice Gender", |
|
|
["male", "female"], |
|
|
index=0, |
|
|
format_func=lambda x: x.capitalize(), |
|
|
help="Select the voice gender for text-to-speech" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
|
|
|
if tts_gender == "male": |
|
|
|
|
|
voice_options = [ |
|
|
"Puck", |
|
|
"Charon", |
|
|
"Fenrir", |
|
|
"Orus", |
|
|
"Achird", |
|
|
"Algenib", |
|
|
"Algieba", |
|
|
"Alnilam", |
|
|
"Enceladus", |
|
|
"Iapetus", |
|
|
"Rasalgethi", |
|
|
"Sadachbia" |
|
|
] |
|
|
default_voice = "Charon" |
|
|
else: |
|
|
|
|
|
voice_options = [ |
|
|
"Aoede", |
|
|
"Kore", |
|
|
"Leda", |
|
|
"Zephyr", |
|
|
"Autonoe", |
|
|
"Callirhoe", |
|
|
"Despina", |
|
|
"Erinome", |
|
|
"Gacrux", |
|
|
"Laomedeia", |
|
|
"Pulcherrima", |
|
|
"Sulafat", |
|
|
"Vindemiatrix", |
|
|
"Achernar" |
|
|
] |
|
|
default_voice = "Aoede" |
|
|
|
|
|
tts_voice = st.selectbox( |
|
|
"Voice Style (Gemini)", |
|
|
voice_options, |
|
|
index=voice_options.index(default_voice), |
|
|
help=f"Select the Gemini voice model for text-to-speech ({tts_gender})" |
|
|
) |
|
|
|
|
|
st.markdown('<div class="section-header">π» Technical Settings</div>', unsafe_allow_html=True) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
|
|
|
tech_options = [ |
|
|
|
|
|
"Python", "Java", "JavaScript", "TypeScript", "C++", "C#", "C", "Go", "Rust", "Swift", |
|
|
"Kotlin", "Scala", "Ruby", "PHP", "Perl", "R", "MATLAB", "Dart", "Objective-C", "Assembly", |
|
|
"Haskell", "Erlang", "Elixir", "F#", "Clojure", "Lua", "Julia", "Groovy", "VB.NET", "COBOL", |
|
|
"Fortran", "Pascal", "Delphi", "Ada", "Prolog", "Lisp", "Scheme", "OCaml", "ML", |
|
|
|
|
|
|
|
|
"HTML", "CSS", "SASS", "LESS", "Bootstrap", "Tailwind CSS", "Material-UI", |
|
|
|
|
|
|
|
|
"React", "Vue.js", "Angular", "Svelte", "Next.js", "Nuxt.js", "Gatsby", "Ember.js", |
|
|
"Backbone.js", "jQuery", "Alpine.js", "Lit", "Stencil", "Ionic", "React Native", |
|
|
"Flutter", "Xamarin", "Cordova", "PhoneGap", |
|
|
|
|
|
|
|
|
"Node.js", "Express.js", "Nest.js", "Django", "Flask", "FastAPI", "Pyramid", "Tornado", |
|
|
"Spring Boot", "Spring MVC", "Struts", "Hibernate", "ASP.NET", "ASP.NET Core", |
|
|
"Ruby on Rails", "Sinatra", "Laravel", "Symfony", "CodeIgniter", "CakePHP", "Zend", |
|
|
"Gin", "Echo", "Fiber", "Actix", "Rocket", "Warp", "Axum", |
|
|
|
|
|
|
|
|
"Android (Java)", "Android (Kotlin)", "iOS (Swift)", "iOS (Objective-C)", |
|
|
|
|
|
|
|
|
"Unity", "Unreal Engine", "Godot", "GameMaker Studio", "Construct", "Phaser", |
|
|
|
|
|
|
|
|
"MySQL", "PostgreSQL", "SQLite", "Microsoft SQL Server", "Oracle Database", |
|
|
"MongoDB", "Redis", "Cassandra", "DynamoDB", "Firebase", "Supabase", |
|
|
"CouchDB", "Neo4j", "InfluxDB", "TimescaleDB", "ClickHouse", "Apache Spark", |
|
|
"Elasticsearch", "Apache Solr", "Amazon RDS", "Google Cloud SQL", |
|
|
|
|
|
|
|
|
"AWS", "Google Cloud Platform", "Microsoft Azure", "Digital Ocean", "Heroku", |
|
|
"Docker", "Kubernetes", "Terraform", "Ansible", "Jenkins", "GitLab CI", "GitHub Actions", |
|
|
"Nginx", "Apache", "Linux", "Ubuntu", "CentOS", "Red Hat", |
|
|
|
|
|
|
|
|
"TensorFlow", "PyTorch", "Scikit-learn", "Pandas", "NumPy", "Matplotlib", "Seaborn", |
|
|
"Jupyter", "Apache Airflow", "Apache Kafka", "Apache Flink", "Hadoop", "Spark", |
|
|
"Tableau", "Power BI", "D3.js", "Plotly", "OpenCV", "Keras", "XGBoost", |
|
|
|
|
|
|
|
|
"Jest", "Mocha", "Chai", "Cypress", "Selenium", "Playwright", "Puppeteer", |
|
|
"JUnit", "TestNG", "Mockito", "PyTest", "unittest", "RSpec", "PHPUnit", |
|
|
|
|
|
|
|
|
"GraphQL", "REST API", "gRPC", "WebSocket", "Apache Kafka", "RabbitMQ", |
|
|
"Blockchain", "Solidity", "Web3", "Ethereum", "Bitcoin", "Smart Contracts", |
|
|
"Microservices", "Serverless", "Lambda Functions", "API Gateway" |
|
|
] |
|
|
|
|
|
|
|
|
tech_options.sort() |
|
|
|
|
|
programming_language = st.selectbox( |
|
|
"Programming Language / Technology", |
|
|
tech_options, |
|
|
index=tech_options.index("Python") if "Python" in tech_options else 0, |
|
|
help="Select the primary programming language, framework, or technology for examples" |
|
|
) |
|
|
with col2: |
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
toggle_hinglish = st.toggle("Enable Hinglish", value=True, help="Enable mixing of Hindi and English") |
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.toggle-label { |
|
|
color: white !important; |
|
|
font-weight: 500; |
|
|
font-size: 14px; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
col3, col4 = st.columns(2) |
|
|
|
|
|
with col3: |
|
|
st.markdown('<span class="toggle-label">Enable Maths Rendering</span>', unsafe_allow_html=True) |
|
|
enable_maths = st.toggle("", value=True, key="maths_toggle") |
|
|
|
|
|
with col4: |
|
|
st.markdown('<span class="toggle-label">Enable Visualizations</span>', unsafe_allow_html=True) |
|
|
enable_visualizations = st.toggle("", value=False, key="viz_toggle") |
|
|
|
|
|
|
|
|
|
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
col1, col2, col3 = st.columns([1, 2, 1]) |
|
|
with col2: |
|
|
submitted = st.form_submit_button("π Generate Course", use_container_width=True) |
|
|
|
|
|
|
|
|
if submitted: |
|
|
|
|
|
topics_to_process = st.session_state.topics_list |
|
|
|
|
|
|
|
|
course_id = DEFAULT_COURSE_ID |
|
|
chapter_id = DEFAULT_CHAPTER_ID |
|
|
|
|
|
|
|
|
valid_topics = [] |
|
|
for topic in topics_to_process: |
|
|
if topic["topic_title"].strip() and topic["chapter_title"].strip(): |
|
|
valid_topics.append(topic) |
|
|
|
|
|
if not valid_topics: |
|
|
st.error("β Please enter at least one topic with both topic title and chapter title") |
|
|
else: |
|
|
|
|
|
with st.spinner(f"π¬ Generating your course... This may take a few moments."): |
|
|
progress_bar = st.progress(0) |
|
|
for i in range(100): |
|
|
time.sleep(0.02) |
|
|
progress_bar.progress(i + 1) |
|
|
|
|
|
|
|
|
user_id = DEFAULT_USER_ID |
|
|
personalization_id = DEFAULT_PERSONALIZATION_ID |
|
|
|
|
|
|
|
|
user_profile = { |
|
|
"personalized": True, |
|
|
"user_name": "System User", |
|
|
"user_age": 25, |
|
|
"user_gender": "male", |
|
|
"user_tech_knowledge": "beginner", |
|
|
"user_preferred_activity": "coding, learning, technology", |
|
|
"user_food": "healthy food, vegetarian", |
|
|
"user_physical_activities": "walking, yoga", |
|
|
"learning_style": "visual", |
|
|
"target_language": target_language, |
|
|
"tts_gender": tts_gender, |
|
|
"tts_voice": tts_voice, |
|
|
"toggle_hinglish": toggle_hinglish, |
|
|
"run_visualization": False, |
|
|
"subtitle": "", |
|
|
"age_group": "18-25" |
|
|
} |
|
|
|
|
|
|
|
|
settings = { |
|
|
"target_language": target_language, |
|
|
"tts_gender": tts_gender, |
|
|
"tts_voice": tts_voice, |
|
|
"toggle_hinglish": toggle_hinglish, |
|
|
"enable_maths": enable_maths, |
|
|
"enable_visualizations": enable_visualizations, |
|
|
"subtitle": "", |
|
|
"programming_language": programming_language, |
|
|
"slide_colour": "blue", |
|
|
"video_type": "base_video" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
topics_data = [] |
|
|
for i, topic in enumerate(valid_topics): |
|
|
topics_data.append({ |
|
|
"topic_id": 10834 + i, |
|
|
"topic_title": topic["topic_title"].strip(), |
|
|
"chapter_id": chapter_id, |
|
|
"chapter_title": topic["chapter_title"].strip(), |
|
|
"course_id": course_id, |
|
|
"course_name": "Base Course", |
|
|
"video_url": f"https://techlearn-dev.s3.ap-south-1.amazonaws.com/course_videos/{course_id}/{chapter_id}/172906{4365+i*50}.mp4", |
|
|
"video_duration": 462 + i*20, |
|
|
"sequence_number": i + 1, |
|
|
}) |
|
|
|
|
|
|
|
|
payload = { |
|
|
"personalization_id": personalization_id, |
|
|
"user_id": user_id, |
|
|
"course_id": course_id, |
|
|
"course_name": "Base Course", |
|
|
"total_videos": len(topics_data), |
|
|
"created_at": datetime.utcnow().isoformat(), |
|
|
"user_profile": user_profile, |
|
|
"topics": topics_data, |
|
|
"settings": settings |
|
|
} |
|
|
|
|
|
|
|
|
if st.session_state.manual_session_id.strip(): |
|
|
payload["manual_session_id"] = st.session_state.manual_session_id.strip() |
|
|
|
|
|
|
|
|
try: |
|
|
headers = { |
|
|
'Content-Type': 'application/json' |
|
|
} |
|
|
|
|
|
response = requests.post(API_ENDPOINT, json=payload, headers=headers, timeout=API_TIMEOUT) |
|
|
|
|
|
if response.status_code == 200: |
|
|
response_data = response.json() |
|
|
session_ids = response_data.get("session_ids", []) |
|
|
|
|
|
st.success(f"π Course generation started successfully!") |
|
|
|
|
|
|
|
|
st.session_state.session_ids.extend(session_ids) |
|
|
|
|
|
|
|
|
st.markdown("### π Generation Summary") |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
with col1: |
|
|
st.markdown(f""" |
|
|
**Course ID**: {course_id} |
|
|
**Chapter ID**: {chapter_id} |
|
|
**Programming Language**: {programming_language.capitalize()} |
|
|
**Target Language**: {target_language.capitalize()} |
|
|
""") |
|
|
with col2: |
|
|
st.markdown(f""" |
|
|
**Voice**: {tts_voice.capitalize()} ({tts_gender.capitalize()}) |
|
|
**Topics Count**: {len(valid_topics)} |
|
|
**Hinglish**: {"Enabled" if toggle_hinglish else "Disabled"} |
|
|
**Manual Session ID**: {st.session_state.manual_session_id if st.session_state.manual_session_id.strip() else "Auto-generated"} |
|
|
""") |
|
|
|
|
|
|
|
|
if session_ids: |
|
|
st.markdown("### π Session Tracking IDs") |
|
|
for i, session_id in enumerate(session_ids, 1): |
|
|
st.markdown(f'<div class="session-info">π <strong>Session {i}:</strong> <code>{session_id}</code></div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
with st.expander("π View Full API Response", expanded=False): |
|
|
st.markdown('<div class="api-response">', unsafe_allow_html=True) |
|
|
st.json(response_data) |
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
st.info(f""" |
|
|
π‘ **Tracking Information** |
|
|
You can track the progress of your course generation using the session IDs above. |
|
|
The processing status will be updated in DynamoDB table: {SESSION_TABLE} |
|
|
Region: {DYNAMODB_REGION} |
|
|
|
|
|
π **Next Steps**: Switch to the "Admin Dashboard" to monitor your session progress in real-time! |
|
|
""") |
|
|
|
|
|
else: |
|
|
st.error(f"β API Error: {response.status_code}") |
|
|
if response.text: |
|
|
st.error(f"**Error Details**: {response.text}") |
|
|
|
|
|
except requests.exceptions.Timeout: |
|
|
st.error("β° Request timed out. Please try again later.") |
|
|
except requests.exceptions.ConnectionError: |
|
|
st.error("π Connection error. Please check your internet connection.") |
|
|
except Exception as e: |
|
|
st.error(f"β API call failed: {str(e)}") |
|
|
|
|
|
|
|
|
with st.expander("π Debug Information", expanded=False): |
|
|
st.warning("Request payload for debugging:") |
|
|
st.json(payload) |