Source code for aita_core.admin

"""
Admin panel for AITA.
"""

import json

import streamlit as st
import pandas as pd
from aita_core.db import (
    get_interaction_stats, get_interactions, get_feedback,
    get_feature_requests, update_feature_request_status,
)
from aita_core.config import get_config


[docs] def check_admin_auth(): if "admin_authenticated" not in st.session_state: st.session_state.admin_authenticated = False # Auto-authenticate if user's Google email is in admin_emails if not st.session_state.admin_authenticated: cfg = get_config() email = st.session_state.get("student_email", "") if email and cfg.admin_emails and email in cfg.admin_emails: st.session_state.admin_authenticated = True return st.session_state.admin_authenticated
def is_admin_user(): """Check if the current user is an admin (without requiring explicit auth).""" cfg = get_config() email = st.session_state.get("student_email", "") if email and cfg.admin_emails and email in cfg.admin_emails: return True # Fallback: anyone can access admin if no admin_emails configured (password-based) if not cfg.admin_emails: return True return False
[docs] def admin_login(): st.title("Admin Login") password = st.text_input("Admin password:", type="password") if st.button("Login"): cfg = get_config() if password == cfg.admin_password: st.session_state.admin_authenticated = True st.rerun() else: st.error("Incorrect password.")
[docs] def admin_dashboard(): st.title("AITA Admin Dashboard") stats = get_interaction_stats() # --- Overview metrics --- col1, col2, col3, col4 = st.columns(4) col1.metric("Total Interactions", stats["total_interactions"]) col2.metric("Unique Students", stats["unique_students"]) col3.metric("Avg Rating", f"{stats['avg_rating']:.1f}" if stats["avg_rating"] else "N/A") col4.metric("Open Requests", stats["open_feature_requests"]) st.markdown("---") # --- Tabs --- tab_history, tab_feedback, tab_requests, tab_settings = st.tabs([ "Interaction History", "Feedback", "Feature Requests", "Course Settings", ]) # --- Interaction History --- with tab_history: st.subheader("Interaction History") # Filters col_filter1, col_filter2 = st.columns(2) with col_filter1: filter_student = st.text_input("Filter by student ID:", key="filter_student") with col_filter2: page_size = st.selectbox("Per page:", [25, 50, 100], key="page_size") student_filter = filter_student.strip() if filter_student else None interactions = get_interactions(limit=page_size, student_id=student_filter) if not interactions: st.info("No interactions recorded yet.") else: for ix in interactions: with st.expander( f"#{ix['id']} | {ix['student_id']} | Week {ix['week']} | " f"{ix['timestamp'][:16]} | " f"{'Rating: ' + str(ix['rating']) if ix['rating'] else 'Unrated'}" ): st.markdown("**Question:**") st.markdown(ix["question"]) st.markdown("**Response:**") st.markdown(ix["response"]) if ix["sources"]: st.markdown(f"**Sources:** {ix['sources']}") # --- Feedback --- with tab_feedback: st.subheader("Student Feedback") feedback_list = get_feedback(limit=100) if not feedback_list: st.info("No feedback submitted yet.") else: for fb in feedback_list: rating_display = "" if fb["rating"]: rating_display = " | " + ("thumbs up" if fb["rating"] == 1 else "thumbs down") with st.expander( f"#{fb['id']} | {fb['student_id']} | {fb['timestamp'][:16]}{rating_display}" ): if fb["comment"]: st.markdown(f"**Comment:** {fb['comment']}") if fb.get("question"): st.markdown(f"**Original question:** {fb['question']}") if fb.get("response"): st.markdown(f"**Bot response:** {fb['response']}") # --- Feature Requests --- with tab_requests: st.subheader("Feature Requests") status_filter = st.selectbox( "Status:", ["all", "open", "in_progress", "done", "wontfix"], key="req_status", ) requests = get_feature_requests( status=status_filter if status_filter != "all" else None ) if not requests: st.info("No feature requests yet.") else: for req in requests: with st.expander( f"#{req['id']} [{req['status']}] {req['title']}{req['student_id']} | {req['timestamp'][:16]}" ): if req["description"]: st.markdown(req["description"]) new_status = st.selectbox( "Update status:", ["open", "in_progress", "done", "wontfix"], index=["open", "in_progress", "done", "wontfix"].index(req["status"]), key=f"status_{req['id']}", ) if new_status != req["status"]: if st.button(f"Save", key=f"save_{req['id']}"): update_feature_request_status(req["id"], new_status) st.success(f"Updated to {new_status}") st.rerun() # --- Course Settings --- with tab_settings: admin_settings() # --- Sidebar --- with st.sidebar: st.title("Admin Panel") if st.button("Back to Chat"): st.session_state.page = "chat" st.rerun() if st.button("Logout Admin"): st.session_state.admin_authenticated = False st.session_state.page = "chat" st.rerun()
def _dict_to_json(d, int_keys=False): """Convert dict to formatted JSON string, with int keys as strings for display.""" if int_keys: d = {str(k): v for k, v in sorted(d.items(), key=lambda x: int(x[0]))} return json.dumps(d, indent=2) def _parse_json_dict(text, int_keys=False): """Parse JSON text to dict, optionally converting keys to int.""" d = json.loads(text) if int_keys: d = {int(k): v for k, v in d.items()} return d
[docs] def admin_settings(): cfg = get_config() st.subheader("Course Settings") st.caption("Changes are saved to disk and persist across restarts.") # --- Quick toggles (outside form for immediate effect) --- st.markdown("#### Semester & Current Week") col_sem1, col_sem2 = st.columns(2) with col_sem1: auto_week = cfg.get_current_week() st.markdown(f"**Auto-detected week:** {auto_week}") with col_sem2: new_test_mode = st.checkbox( "Test Mode (show week slider in chat sidebar)", value=cfg.test_mode, key="test_mode_toggle", ) if new_test_mode != cfg.test_mode: cfg.save_overrides({"test_mode": new_test_mode}) st.rerun() st.markdown("---") with st.form("settings_form"): # --- Course Identity --- st.markdown("#### Course Identity") course_name = st.text_input("Course Name", value=cfg.course_name) course_short_name = st.text_input("Short Name", value=cfg.course_short_name) course_description = st.text_area( "Description (shown on login page)", value=cfg.course_description, height=80, ) st.markdown("---") # --- Semester Start --- st.markdown("#### Semester Start") semester_start = st.text_input( "Semester Start Date (YYYY-MM-DD)", value=cfg.semester_start, help="First day of week 1. Current week is auto-computed from this date.", ) st.markdown("---") # --- System Prompt --- st.markdown("#### System Prompt") system_prompt = st.text_area("System Prompt", value=cfg.system_prompt, height=300) st.markdown("---") # --- LLM Settings --- st.markdown("#### LLM Settings") col1, col2, col3 = st.columns(3) with col1: llm_model = st.text_input("LLM Model", value=cfg.llm_model) with col2: llm_temperature = st.number_input( "Temperature", value=float(cfg.llm_temperature), min_value=0.0, max_value=2.0, step=0.1, ) with col3: retrieval_k = st.number_input( "Retrieval K", value=int(cfg.retrieval_k), min_value=1, max_value=20, step=1, ) col4, col5, col6 = st.columns(3) with col4: chunk_size = st.number_input( "Chunk Size", value=int(cfg.chunk_size), min_value=256, max_value=8192, step=256, ) with col5: chunk_overlap = st.number_input( "Chunk Overlap", value=int(cfg.chunk_overlap), min_value=0, max_value=2048, step=64, ) with col6: embedding_model = st.text_input("Embedding Model", value=cfg.embedding_model) st.caption( "Changes to embedding model, chunk size, and chunk overlap " "require re-ingestion of documents to take effect." ) st.markdown("---") # --- Textbook --- st.markdown("#### Textbook") textbook_url = st.text_input("Textbook URL", value=cfg.textbook_url) textbook_ch_json = st.text_area( "Chapter → Week mapping (JSON)", value=json.dumps(cfg.textbook_chapter_to_week, indent=2), height=200, ) st.markdown("---") # --- Week Schedule --- st.markdown("#### Week Schedule") week_topics_json = st.text_area( "Week Topics — {week_number: [topic1, topic2, ...]}", value=_dict_to_json(cfg.week_topics, int_keys=True), height=300, ) st.markdown("---") # --- Exam Scope --- st.markdown("#### Exam Scope") st.caption( "Defines which weeks each exam covers. Used to scope study guides " "and exam review responses. Click 'Auto-detect' to populate from " "week topics, then adjust as needed." ) exam_scope_default = json.dumps(cfg.exam_scope, indent=2) if cfg.exam_scope else "{}" exam_scope_value = st.session_state.get("_exam_scope_json", exam_scope_default) exam_scope_json = st.text_area( 'Exam Scope — {"Exam Name": {"week_start": N, "week_end": M}, ...}', value=exam_scope_value, height=150, key="exam_scope_editor", ) if cfg.exam_scope: for exam_name, scope in sorted(cfg.exam_scope.items()): topics = cfg.get_exam_topics(exam_name) if topics: st.caption( f"**{exam_name}** (weeks {scope['week_start']}-{scope['week_end']}): " f"{', '.join(topics)}" ) st.markdown("---") # --- Content Mappings --- st.markdown("#### Content Mappings") st.caption( "These control which week each piece of content is available. " "Changes to topic/lab mappings require re-ingestion." ) col_m1, col_m2 = st.columns(2) with col_m1: hw_json = st.text_area( "HW → Week — {hw_num: week}", value=_dict_to_json(cfg.hw_num_to_week, int_keys=True), height=200, ) with col_m2: topic_json = st.text_area( "Topic → Week — {topic_num: week}", value=_dict_to_json(cfg.topic_num_to_week, int_keys=True), height=200, ) col_m3, col_m4 = st.columns(2) with col_m3: lab_json = st.text_area( "Lab → Week — {lab_num: week}", value=_dict_to_json(cfg.lab_num_to_week, int_keys=True), height=150, ) with col_m4: study_json = st.text_area( "Study Guide → Week — {name: week}", value=json.dumps(cfg.study_guide_to_week, indent=2), height=150, ) st.markdown("---") # --- Example Prompts --- st.markdown("#### Example Prompts") example_json = st.text_area( "Example Prompts by Week — {week: [prompt1, prompt2, ...]}", value=_dict_to_json(cfg.example_prompts, int_keys=True), height=300, ) submitted = st.form_submit_button("Save Settings", type="primary") if submitted: try: overrides = { "course_name": course_name, "course_short_name": course_short_name, "course_description": course_description, "semester_start": semester_start, "system_prompt": system_prompt, "llm_model": llm_model, "llm_temperature": llm_temperature, "retrieval_k": retrieval_k, "chunk_size": chunk_size, "chunk_overlap": chunk_overlap, "embedding_model": embedding_model, "textbook_url": textbook_url, "textbook_chapter_to_week": json.loads(textbook_ch_json), "week_topics": _parse_json_dict(week_topics_json, int_keys=True), "hw_num_to_week": _parse_json_dict(hw_json, int_keys=True), "topic_num_to_week": _parse_json_dict(topic_json, int_keys=True), "lab_num_to_week": _parse_json_dict(lab_json, int_keys=True), "study_guide_to_week": json.loads(study_json), "exam_scope": json.loads(exam_scope_json), "example_prompts": _parse_json_dict(example_json, int_keys=True), } cfg.save_overrides(overrides) # Clear auto-detect session state after save st.session_state.pop("_exam_scope_json", None) st.success("Settings saved! Changes take effect immediately (except embedding/chunk settings).") except json.JSONDecodeError as e: st.error(f"Invalid JSON: {e}") except (ValueError, TypeError) as e: st.error(f"Invalid value: {e}") # Auto-detect button (outside form so it can trigger rerun) if st.button("Auto-detect Exam Scope from Week Topics"): detected = cfg.auto_detect_exam_scope() st.session_state["_exam_scope_json"] = json.dumps(detected, indent=2) st.info( f"Detected {len(detected)} exam(s): {', '.join(detected.keys())}. " "Review above and click 'Save Settings' to apply." ) st.rerun()
[docs] def admin_page(): if not check_admin_auth(): admin_login() else: admin_dashboard()